@monoes/monomindcli 1.12.0 → 1.14.0

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 (61) hide show
  1. package/.claude/agents/generated/churn-analyst.md +53 -0
  2. package/.claude/agents/generated/code-reviewer.md +55 -0
  3. package/.claude/agents/generated/code-validator.md +57 -0
  4. package/.claude/agents/generated/complexity-scanner.md +56 -0
  5. package/.claude/agents/generated/devbot-orchestrator.md +58 -0
  6. package/.claude/agents/generated/devbot-planner.md +63 -0
  7. package/.claude/agents/generated/impact-assessor.md +54 -0
  8. package/.claude/commands/mastermind/master.md +88 -24
  9. package/.claude/helpers/control-start.cjs +60 -1
  10. package/.claude/helpers/event-logger.cjs +43 -2
  11. package/.claude/helpers/handlers/capture-handler.cjs +336 -0
  12. package/.claude/helpers/handlers/route-handler.cjs +20 -13
  13. package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
  14. package/.claude/helpers/hook-handler.cjs +57 -1
  15. package/.claude/helpers/intelligence.cjs +129 -57
  16. package/.claude/helpers/memory-palace.cjs +461 -0
  17. package/.claude/helpers/memory.cjs +134 -15
  18. package/.claude/helpers/metrics-db.mjs +87 -0
  19. package/.claude/helpers/router.cjs +296 -41
  20. package/.claude/helpers/session.cjs +107 -32
  21. package/.claude/helpers/statusline.cjs +138 -2
  22. package/.claude/helpers/toggle-statusline.cjs +73 -0
  23. package/.claude/helpers/token-tracker.cjs +934 -0
  24. package/.claude/helpers/utils/monograph.cjs +39 -4
  25. package/.claude/helpers/utils/telemetry.cjs +3 -3
  26. package/.claude/skills/mastermind/createorg.md +227 -16
  27. package/.claude/skills/mastermind/idea.md +15 -3
  28. package/.claude/skills/mastermind/runorg.md +2 -1
  29. package/dist/src/commands/doctor.d.ts.map +1 -1
  30. package/dist/src/commands/doctor.js +96 -4
  31. package/dist/src/commands/doctor.js.map +1 -1
  32. package/dist/src/commands/index.js +2 -0
  33. package/dist/src/commands/org.d.ts +4 -0
  34. package/dist/src/commands/org.d.ts.map +1 -0
  35. package/dist/src/commands/org.js +93 -0
  36. package/dist/src/commands/org.js.map +1 -0
  37. package/dist/src/mcp-tools/memory-tools.js +6 -6
  38. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  39. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  40. package/dist/src/mcp-tools/monograph-tools.js +329 -37
  41. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  42. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  43. package/dist/src/mcp-tools/session-tools.js +9 -10
  44. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  45. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  46. package/dist/src/mcp-tools/task-tools.js +7 -8
  47. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  48. package/dist/src/mcp-tools/types.d.ts +1 -0
  49. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  50. package/dist/src/mcp-tools/types.js +49 -0
  51. package/dist/src/mcp-tools/types.js.map +1 -1
  52. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  53. package/dist/src/services/worker-daemon.js +295 -5
  54. package/dist/src/services/worker-daemon.js.map +1 -1
  55. package/dist/src/transfer/serialization/cfp.js +1 -1
  56. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  57. package/dist/src/ui/dashboard.html +2235 -178
  58. package/dist/src/ui/orgs.html +1 -0
  59. package/dist/src/ui/server.mjs +532 -133
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +1 -1
@@ -185,6 +185,8 @@ const sseClients = new Set();
185
185
  const mmSseClients = new Set();
186
186
  // Active org run tracking: org -> runId (enables event routing for orgs without runId in payload)
187
187
  const activeOrgRuns = new Map();
188
+ // Active session tracking: org -> {sessionId, ts} (enables linking agent events to sessions)
189
+ const activeSessionsByOrg = new Map();
188
190
 
189
191
  // Returns the shared git directory parent so run files survive branch switches and
190
192
  // are shared across all worktrees. In a worktree, .git is a FILE pointing to the
@@ -363,6 +365,207 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
363
365
  return out;
364
366
  }
365
367
 
368
+ // ── handleMastermindEvent ─────────────────────────────────────────────────
369
+ // Extracted from the request dispatcher to reduce cyclomatic complexity.
370
+ // Handles POST /api/mastermind/event: parses body, enriches with runId/session,
371
+ // persists to JSONL files, broadcasts to SSE clients, returns {ok:true}.
372
+ async function handleMastermindEvent(req, res) {
373
+ let body = '';
374
+ for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
375
+ let event = {};
376
+ try { event = JSON.parse(body); } catch (_) {}
377
+ event.ts = event.ts || Date.now();
378
+ // Use project path from event if provided (multi-project support).
379
+ // Security: path.isAbsolute() alone is insufficient — an attacker can
380
+ // supply event.project="/etc" and cause writes to system directories.
381
+ // Only accept paths that resolve to an existing directory AND are not
382
+ // the filesystem root (/), AND are not obviously system paths.
383
+ // Cap to 4096 chars to prevent OOM from huge path strings.
384
+ const _rawProject = event.project;
385
+ let eventProject = null;
386
+ if (typeof _rawProject === 'string' && _rawProject.length > 0 && _rawProject.length <= 4096
387
+ && path.isAbsolute(_rawProject)) {
388
+ // Reject filesystem root and common system directories
389
+ const _norm = path.resolve(_rawProject);
390
+ const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
391
+ if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
392
+ eventProject = _norm;
393
+ }
394
+ }
395
+ const root = eventProject || projectDir || process.cwd();
396
+ const dataDir = path.join(root, 'data');
397
+ try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
398
+ // Track known project dirs for aggregated session listing
399
+ if (eventProject) {
400
+ const knownFile = path.join(projectDir || process.cwd(), 'data', 'known-projects.json');
401
+ try {
402
+ let known = [];
403
+ try { known = JSON.parse(fs.readFileSync(knownFile, 'utf8')); } catch (_) {}
404
+ if (!known.includes(eventProject)) { known.push(eventProject); fs.writeFileSync(knownFile, JSON.stringify(known)); }
405
+ } catch (_) {}
406
+ }
407
+ // Track active runs and enrich event with runId BEFORE persisting so the JSONL replay
408
+ // on SSE reconnect contains the same enriched event that live clients received.
409
+ // Previously this was done AFTER the appendFileSync, causing org:comms events stored in
410
+ // mastermind-events.jsonl to lack runId — _odtHandleLiveEvent dropped them on reconnect.
411
+ if (event.org) {
412
+ const _orgKey = String(event.org).trim();
413
+ // Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
414
+ if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
415
+ else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
416
+ if (event.type === 'run:complete' || event.type === 'org:complete') activeOrgRuns.delete(_orgKey);
417
+ // Persist active-run.json so capture-handler.cjs can find the current org/runId without HTTP calls
418
+ try {
419
+ const _captureDir = path.join(root, '.monomind', 'capture');
420
+ const _activeRunFile = path.join(_captureDir, 'active-run.json');
421
+ if (event.type === 'run:start' && event.org && event.runId) {
422
+ fs.mkdirSync(_captureDir, { recursive: true });
423
+ fs.writeFileSync(_activeRunFile, JSON.stringify({ org: String(event.org).trim(), runId: String(event.runId).trim(), ts: Date.now() }));
424
+ } else if ((event.type === 'run:complete' || event.type === 'org:complete') && fs.existsSync(_activeRunFile)) {
425
+ fs.unlinkSync(_activeRunFile);
426
+ }
427
+ } catch(_e) {}
428
+ }
429
+ try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
430
+ // Persist to git-safe run file (survives branch switches + shared across worktrees)
431
+ if (event.org && event.runId) {
432
+ try {
433
+ const _orn = String(event.org).trim();
434
+ const _rid = String(event.runId).trim();
435
+ if (_orn.length > 0 && _orn.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_orn)
436
+ && _rid.length > 0 && _rid.length <= 80 && /^[a-z0-9][a-z0-9_-]*$/i.test(_rid)) {
437
+ const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
438
+ const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
439
+ fs.mkdirSync(_runDir, { recursive: true });
440
+ fs.appendFileSync(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
441
+ // agent:usage — persist per-role token/cost data to state.json (accumulated across runs)
442
+ if (event.type === 'agent:usage' && event.role) {
443
+ try {
444
+ const _arole = String(event.role).trim();
445
+ if (_arole.length > 0 && _arole.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_arole)) {
446
+ const _stateFile = path.join(root, '.monomind', 'orgs', `${_orn}-state.json`);
447
+ let _st = {};
448
+ try { _st = JSON.parse(fs.readFileSync(_stateFile, 'utf8')); } catch(_e) {}
449
+ if (!_st.agents) _st.agents = {};
450
+ const _ex = _st.agents[_arole] || {};
451
+ _st.agents[_arole] = {
452
+ ..._ex,
453
+ tokens_in: (_ex.tokens_in || 0) + (Number(event.tokens_in) || 0),
454
+ tokens_out: (_ex.tokens_out || 0) + (Number(event.tokens_out) || 0),
455
+ total_cost_usd: (_ex.total_cost_usd || 0) + (Number(event.cost_usd) || 0),
456
+ lastUpdated: event.ts,
457
+ };
458
+ fs.writeFileSync(_stateFile, JSON.stringify(_st, null, 2));
459
+ }
460
+ } catch(_e) {}
461
+ }
462
+ // Solution 3: dedicated conversation log — org:comms only, for easy replay
463
+ if (event.type === 'org:comms') {
464
+ const _conv = { ts: event.ts, run_id: _rid, from: event.from, to: event.to, msg: event.msg };
465
+ fs.appendFileSync(path.join(_runDir, `${_rid}.convs.jsonl`), JSON.stringify(_conv) + '\n');
466
+ // Also write to org-level threads.jsonl so the dashboard Threads tab shows agent conversations
467
+ const _orgThreadsFile = path.join(root, '.monomind', 'orgs', `${_orn}-threads.jsonl`);
468
+ const _thread = { type: 'message', id: `${_rid}-${event.ts}`, run_id: _rid, ts: event.ts, from: event.from, to: event.to, msg: event.msg, subject: `Run ${_rid}` };
469
+ try { fs.appendFileSync(_orgThreadsFile, JSON.stringify(_thread) + '\n'); } catch(_) {}
470
+ }
471
+ }
472
+ } catch (_) {}
473
+ }
474
+ // ── Active session tracking: link org:comms / agent:usage events to current session ──
475
+ // This must run BEFORE session persistence so events without session get enriched.
476
+ try {
477
+ const _evOrg = event.org ? String(event.org).trim() : null;
478
+ if (event.type === 'session:start' && event.session && _evOrg) {
479
+ activeSessionsByOrg.set(_evOrg, { sessionId: String(event.session), ts: event.ts || Date.now() });
480
+ // Write active-session.json so capture-handler.cjs can read it without HTTP
481
+ try {
482
+ const _captureDir = path.join(root, '.monomind', 'capture');
483
+ fs.mkdirSync(_captureDir, { recursive: true });
484
+ fs.writeFileSync(path.join(_captureDir, 'active-session.json'),
485
+ JSON.stringify({ org: _evOrg, sessionId: String(event.session), ts: Date.now() }));
486
+ } catch(_) {}
487
+ } else if (event.type === 'session:complete' && _evOrg) {
488
+ activeSessionsByOrg.delete(_evOrg);
489
+ try { fs.unlinkSync(path.join(root, '.monomind', 'capture', 'active-session.json')); } catch(_) {}
490
+ }
491
+ // Enrich events that have org but no session (agent:usage, org:comms, agent:spawn, intercom)
492
+ if (_evOrg && !event.session && activeSessionsByOrg.has(_evOrg)) {
493
+ event.session = activeSessionsByOrg.get(_evOrg).sessionId;
494
+ }
495
+ } catch(_) {}
496
+ // ── Per-session JSONL persistence (append-only, O(1) per event) ──────────
497
+ // Replaces the old monolithic mastermind-sessions.json (O(N) read+write per event).
498
+ // Format: data/sessions/<sessionId>.jsonl + data/sessions/_index.json
499
+ try {
500
+ const _sid = String(event.session || '').trim();
501
+ if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]+$/.test(_sid)) {
502
+ const sessDir = path.join(dataDir, 'sessions');
503
+ fs.mkdirSync(sessDir, { recursive: true });
504
+ // Append event to per-session JSONL (O(1), no read)
505
+ fs.appendFileSync(path.join(sessDir, `${_sid}.jsonl`), JSON.stringify(event) + '\n');
506
+ // Update lightweight index (id, ts, prompt, status, org, startedAt, endedAt, domains only)
507
+ const indexFile = path.join(sessDir, '_index.json');
508
+ let _idx = [];
509
+ try { _idx = JSON.parse(fs.readFileSync(indexFile, 'utf8')); } catch(_) {}
510
+ const _entry = _idx.find(e => e.id === _sid);
511
+ if (event.type === 'session:start') {
512
+ if (!_entry) {
513
+ _idx.unshift({ id: _sid, ts: event.ts, prompt: event.prompt || '', status: 'running',
514
+ org: event.org || '', startedAt: event.ts, domains: [] });
515
+ if (_idx.length > 2000) _idx = _idx.slice(0, 2000);
516
+ }
517
+ } else if (_entry) {
518
+ if (event.type === 'session:complete') { _entry.status = event.status || 'complete'; _entry.endedAt = event.ts; }
519
+ if (event.type === 'domain:dispatch' && event.domain) {
520
+ _entry.domains = _entry.domains || [];
521
+ if (!_entry.domains.includes(event.domain)) _entry.domains.push(event.domain);
522
+ }
523
+ if (event.type === 'agent:usage' || event.type === 'agent:spawn' || event.type === 'agent:complete') {
524
+ _entry.hasAgents = true;
525
+ }
526
+ }
527
+ fs.writeFileSync(indexFile, JSON.stringify(_idx));
528
+ }
529
+ } catch (_) {}
530
+ // ── Legacy mastermind-sessions.json (kept for backwards compat, read by old clients) ──
531
+ try {
532
+ const sessFile = path.join(dataDir, 'mastermind-sessions.json');
533
+ let sessions = [];
534
+ try { sessions = JSON.parse(fs.readFileSync(sessFile, 'utf8')); } catch (_) {}
535
+ if (event.type === 'session:start' && event.session) {
536
+ if (!sessions.find(s => s.id === event.session)) {
537
+ sessions.unshift({ id: event.session, ts: event.ts, prompt: event.prompt || '',
538
+ status: 'running', org: event.org || '', domains: [], startedAt: event.ts });
539
+ }
540
+ } else if (event.session) {
541
+ const s = sessions.find(s => s.id === event.session);
542
+ if (s) {
543
+ if (event.type === 'session:complete') { s.status = event.status || 'complete'; s.endedAt = event.ts; }
544
+ if (event.type === 'domain:dispatch' && event.domain && !s.domains?.includes(event.domain))
545
+ (s.domains = s.domains || []).push(event.domain);
546
+ }
547
+ }
548
+ fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 500)));
549
+ } catch (_) {}
550
+ // For org:stop events, write a stop marker the boss agent can detect
551
+ if (event.type === 'org:stop' && event.org) {
552
+ try {
553
+ const orgName = String(event.org).trim();
554
+ // Validate before any filesystem use — reject rather than strip
555
+ if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
556
+ const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
557
+ fs.mkdirSync(stopDir, { recursive: true });
558
+ fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
559
+ }
560
+ } catch (_) {}
561
+ }
562
+ // Broadcast to all mastermind SSE clients
563
+ const msg = `data: ${JSON.stringify(event)}\n\n`;
564
+ for (const c of mmSseClients) { try { c.write(msg); } catch (_) { mmSseClients.delete(c); } }
565
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
566
+ res.end('{"ok":true}');
567
+ }
568
+
366
569
  const server = http.createServer(async (req, res) => {
367
570
  const url = req.url.split('?')[0];
368
571
 
@@ -409,12 +612,22 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
409
612
  const cwd = projectDir || process.cwd();
410
613
  const name = gitExec('git config user.name', { cwd, encoding: 'utf8' }).trim();
411
614
  const email = gitExec('git config user.email', { cwd, encoding: 'utf8' }).trim();
615
+ let remoteUrl = '';
616
+ try { remoteUrl = gitExec('git remote get-url origin', { cwd, encoding: 'utf8' }).trim(); } catch {}
617
+ // Normalise SSH remote to HTTPS URL for browser linking
618
+ if (remoteUrl.startsWith('git@')) {
619
+ remoteUrl = remoteUrl.replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '');
620
+ } else if (remoteUrl.endsWith('.git')) {
621
+ remoteUrl = remoteUrl.slice(0, -4);
622
+ }
623
+ let branch = '';
624
+ try { branch = gitExec('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(); } catch {}
412
625
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
413
- res.end(JSON.stringify({ name, email, cwd }));
626
+ res.end(JSON.stringify({ name, email, cwd, remoteUrl, branch }));
414
627
  } catch (_) {
415
628
  const cwd2 = projectDir || process.cwd();
416
629
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
417
- res.end(JSON.stringify({ name: '', email: '', cwd: cwd2 }));
630
+ res.end(JSON.stringify({ name: '', email: '', cwd: cwd2, remoteUrl: '', branch: '' }));
418
631
  }
419
632
  return;
420
633
  }
@@ -498,7 +711,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
498
711
  .map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
499
712
  .filter(Boolean)
500
713
  .sort((a, b) => b.mtime - a.mtime)
501
- .slice(0, 15);
714
+ .slice(0, 50);
502
715
  } catch {}
503
716
 
504
717
  const sessions = [];
@@ -631,6 +844,69 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
631
844
  return;
632
845
  }
633
846
 
847
+ // ------------------------------------------------------- GET /api/recent-events
848
+ if (req.method === 'GET' && url === '/api/recent-events') {
849
+ try {
850
+ const qs = new URL(req.url, 'http://localhost').searchParams;
851
+ const dir = qs.get('dir') || projectDir || process.cwd();
852
+ const limit = Math.min(parseInt(qs.get('limit') || '50', 10), 200);
853
+ const d = path.resolve(dir || process.cwd());
854
+ const slug = d.replace(/\//g, '-');
855
+ const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
856
+ let sessionFiles = [];
857
+ try {
858
+ sessionFiles = fs.readdirSync(projectClaudeDir)
859
+ .filter(f => f.endsWith('.jsonl'))
860
+ .map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
861
+ .filter(Boolean)
862
+ .sort((a, b) => b.mtime - a.mtime)
863
+ .slice(0, 5); // check last 5 sessions
864
+ } catch {}
865
+
866
+ const events = [];
867
+ const HOOK_RE = /^<(local-command-|command-name>|command-message>)/;
868
+ for (const { f } of sessionFiles) {
869
+ const fp = path.join(projectClaudeDir, f);
870
+ const sessId = f.replace('.jsonl', '');
871
+ try {
872
+ const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean).slice(-200);
873
+ for (const line of lines) {
874
+ let e; try { e = JSON.parse(line); } catch { continue; }
875
+ if (e.type === 'assistant') {
876
+ const content = e.message?.content || [];
877
+ for (const block of content) {
878
+ if (block?.type === 'tool_use') {
879
+ events.push({ kind: 'tool', ts: e.timestamp, tool: block.name, session: sessId });
880
+ }
881
+ }
882
+ } else if (e.type === 'user') {
883
+ const content = e.message?.content || [];
884
+ for (const block of content) {
885
+ if (block?.type === 'text' && block.text?.trim() && !HOOK_RE.test(block.text.trim())) {
886
+ events.push({ kind: 'user', ts: e.timestamp, text: block.text.slice(0, 120), session: sessId });
887
+ }
888
+ }
889
+ }
890
+ }
891
+ } catch {}
892
+ }
893
+
894
+ // sort by ts desc, take limit
895
+ events.sort((a, b) => {
896
+ const ta = a.ts ? new Date(a.ts).getTime() : 0;
897
+ const tb = b.ts ? new Date(b.ts).getTime() : 0;
898
+ return tb - ta;
899
+ });
900
+
901
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
902
+ res.end(JSON.stringify({ events: events.slice(0, limit) }));
903
+ } catch (err) {
904
+ res.writeHead(500, { 'Content-Type': 'application/json' });
905
+ res.end(JSON.stringify({ error: err.message }));
906
+ }
907
+ return;
908
+ }
909
+
634
910
  // ------------------------------------------------------- GET /api/tool-errors
635
911
  if (req.method === 'GET' && url === '/api/tool-errors') {
636
912
  try {
@@ -3144,6 +3420,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3144
3420
  const stopTs = lastStop ? (JSON.parse(lastStop).ts || 0) : 0;
3145
3421
  running = startTs > stopTs;
3146
3422
  }
3423
+ // Also check in-memory activeOrgRuns so the list reflects LIVE immediately after launch
3424
+ const _lOrgName = cfg.name || '';
3425
+ if (!running && _lOrgName && activeOrgRuns.has(_lOrgName)) running = true;
3147
3426
  orgs.push({ name: cfg.name, goal: cfg.goal, roles: Array.isArray(cfg.roles) ? cfg.roles : [], topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
3148
3427
  } catch(_) {}
3149
3428
  }
@@ -3240,9 +3519,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
3240
3519
  const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || { routines: [] };
3241
3520
  const approvalsData = readJsonSafe(path.join(orgsDir, `${orgName}-approvals.json`)) || { approvals: [] };
3242
3521
 
3243
- // Check running status from stop file absence + state
3522
+ // Check running status: stop file absence AND (in-memory activeOrgRuns OR state-file agents)
3244
3523
  const stopFile = path.join(orgsDir, '.stops', `${orgName}.stop`);
3245
- const running = !fs.existsSync(stopFile) && Object.values(state.agents || {}).some(a => a.status === 'running');
3524
+ const running = !fs.existsSync(stopFile) && (activeOrgRuns.has(orgName) || Object.values(state.agents || {}).some(a => a.status === 'running'));
3246
3525
 
3247
3526
  // Read real tasks from the task store and group by status column
3248
3527
  const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
@@ -4001,13 +4280,45 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4001
4280
  total_cost_usd: s.total_cost_usd || 0,
4002
4281
  }));
4003
4282
  } catch(_) {}
4004
- // Also include roles from org config if state is empty
4005
- if (!agents.length) {
4283
+ // Scan org run jsonl files for agent:usage events (fallback when state.json has no token data)
4284
+ const _hasTokenData = agents.some(a => a.tokens_in > 0 || a.tokens_out > 0 || a.total_cost_usd > 0);
4285
+ if (!_hasTokenData) {
4006
4286
  try {
4007
- const org = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
4008
- agents = (org.roles || []).map(r => ({ id: r.id, title: r.title, tokens_in: 0, tokens_out: 0, total_cost_usd: 0 }));
4287
+ const _runsDir = path.join(base, orgName, 'runs');
4288
+ if (fs.existsSync(_runsDir)) {
4289
+ const _usageByRole = {};
4290
+ for (const f of fs.readdirSync(_runsDir)) {
4291
+ if (!f.endsWith('.jsonl')) continue;
4292
+ const lines = fs.readFileSync(path.join(_runsDir, f), 'utf8').split('\n').filter(Boolean);
4293
+ for (const l of lines) {
4294
+ try {
4295
+ const ev = JSON.parse(l);
4296
+ if (ev.type === 'agent:usage' && ev.role) {
4297
+ const role = String(ev.role).trim();
4298
+ if (!_usageByRole[role]) _usageByRole[role] = { tokens_in: 0, tokens_out: 0, total_cost_usd: 0 };
4299
+ _usageByRole[role].tokens_in += Number(ev.tokens_in) || 0;
4300
+ _usageByRole[role].tokens_out += Number(ev.tokens_out) || 0;
4301
+ _usageByRole[role].total_cost_usd += Number(ev.cost_usd) || 0;
4302
+ }
4303
+ } catch(_) {}
4304
+ }
4305
+ }
4306
+ if (Object.keys(_usageByRole).length > 0) {
4307
+ // Merge usage into agents list; preserve role titles
4308
+ agents = agents.map(a => {
4309
+ const u = _usageByRole[a.id] || {};
4310
+ return { ...a, tokens_in: u.tokens_in || 0, tokens_out: u.tokens_out || 0, total_cost_usd: u.total_cost_usd || 0 };
4311
+ });
4312
+ // Add any roles that appeared in events but aren't in config
4313
+ for (const [role, u] of Object.entries(_usageByRole)) {
4314
+ if (!agents.find(a => a.id === role)) agents.push({ id: role, title: role, ...u });
4315
+ }
4316
+ }
4317
+ }
4009
4318
  } catch(_) {}
4010
4319
  }
4320
+ // Do NOT fall back to zero-value role stubs — empty agents array is the honest signal
4321
+ // that no usage has been tracked yet; the UI shows "No cost data" rather than $0.0000 rows.
4011
4322
  res.writeHead(200, { 'Content-Type': 'application/json' });
4012
4323
  res.end(JSON.stringify({ ...budgetData, agents }));
4013
4324
  } catch(_) { res.writeHead(500); res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}'); }
@@ -4026,11 +4337,20 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4026
4337
  try {
4027
4338
  const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
4028
4339
  threads = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
4340
+ // Group 'message' entries (from org:comms) by run_id into synthetic thread objects
4341
+ const msgsByRun = {};
4342
+ threads.filter(t => t.type === 'message').forEach(m => {
4343
+ const rid = m.run_id || 'unknown';
4344
+ if (!msgsByRun[rid]) msgsByRun[rid] = { id: `thread-${rid}`, type: 'thread', subject: `Run ${rid}`, run_id: rid, createdAt: m.ts, messages: [] };
4345
+ msgsByRun[rid].messages.push({ from: m.from, to: m.to, msg: m.msg, ts: m.ts });
4346
+ });
4347
+ const syntheticThreads = Object.values(msgsByRun).map(t => ({ ...t, messageCount: t.messages.length, author: t.messages[0]?.from || null }));
4029
4348
  threads = threads.filter(t => t.type === 'thread' || !t.type).map(t => ({
4030
4349
  ...t,
4031
4350
  author: t.author || t.authorName || t.createdBy || t.authorId || null,
4032
4351
  messageCount: t.messageCount != null ? t.messageCount : (Array.isArray(t.messages) ? t.messages.length : (typeof t.messages === 'number' ? t.messages : null)),
4033
4352
  }));
4353
+ threads = [...threads, ...syntheticThreads];
4034
4354
  } catch(_) {}
4035
4355
  res.writeHead(200, { 'Content-Type': 'application/json' });
4036
4356
  res.end(JSON.stringify({ threads }));
@@ -4082,15 +4402,48 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4082
4402
  return;
4083
4403
  }
4084
4404
 
4085
- // GET /api/org/:name/routines — read org routines
4405
+ // GET /api/org/:name/routines — read org routines (falls back to synthesizing from org config's loop object)
4086
4406
  if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
4087
4407
  try {
4088
4408
  const orgName = decodeURIComponent(url.split('/')[3]);
4089
4409
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
4090
4410
  const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
4091
- const routinesFile = path.join(path.resolve(_routinesQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs', `${orgName}-routines.json`);
4411
+ const _routinesBase = path.resolve(_routinesQs.get('dir') || projectDir || process.cwd());
4412
+ const routinesFile = path.join(_routinesBase, '.monomind', 'orgs', `${orgName}-routines.json`);
4092
4413
  let data = { routines: [] };
4093
4414
  try { data = JSON.parse(fs.readFileSync(routinesFile, 'utf8')); } catch(_) {}
4415
+ // Synthesize routines from org config's loop/schedule settings when no explicit routines are defined
4416
+ if (!data.routines || !data.routines.length) {
4417
+ try {
4418
+ const orgCfg = JSON.parse(fs.readFileSync(path.join(_routinesBase, '.monomind', 'orgs', `${orgName}.json`), 'utf8'));
4419
+ const loop = orgCfg.loop;
4420
+ if (loop && (loop.poll_interval_minutes || loop.interval_minutes)) {
4421
+ const intervalMin = loop.poll_interval_minutes || loop.interval_minutes;
4422
+ data.routines = [{
4423
+ name: `${orgName}-cycle`,
4424
+ description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org iteration cycle',
4425
+ schedule: `every ${intervalMin}m`,
4426
+ cron: null,
4427
+ enabled: orgCfg.status === 'active',
4428
+ status: orgCfg.status || 'stopped',
4429
+ prompt_file: loop.run_prompt_file || null,
4430
+ source: 'loop-config',
4431
+ lastRun: null,
4432
+ }];
4433
+ } else if (orgCfg.schedule) {
4434
+ data.routines = [{
4435
+ name: `${orgName}-schedule`,
4436
+ description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org scheduled run',
4437
+ schedule: String(orgCfg.schedule),
4438
+ cron: null,
4439
+ enabled: orgCfg.status === 'active',
4440
+ status: orgCfg.status || 'stopped',
4441
+ source: 'schedule-config',
4442
+ lastRun: null,
4443
+ }];
4444
+ }
4445
+ } catch(_) {}
4446
+ }
4094
4447
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4095
4448
  res.end(JSON.stringify({ routines: data.routines || [] }));
4096
4449
  } catch(_) { res.writeHead(500); res.end('{"routines":[]}'); }
@@ -4331,7 +4684,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4331
4684
  const allLines = raw.split('\n').filter(Boolean);
4332
4685
  const eventCount = allLines.length;
4333
4686
  const parse = l => { try { return JSON.parse(l); } catch { return null; } };
4334
- const headEvents = allLines.slice(0, 5).map(parse).filter(Boolean);
4687
+ const headEvents = allLines.slice(0, 10).map(parse).filter(Boolean);
4335
4688
  const tailEvents = allLines.slice(-5).map(parse).filter(Boolean);
4336
4689
  const first = headEvents.find(e => e.type === 'run:start') || headEvents[0];
4337
4690
  const last = tailEvents.slice().reverse().find(e => e.type === 'run:complete' || e.type === 'org:complete');
@@ -4339,9 +4692,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4339
4692
  const lastEvent = tailEvents[tailEvents.length - 1] || headEvents[headEvents.length - 1];
4340
4693
  const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
4341
4694
  const isStale = !last && ageMs > 30 * 60 * 1000;
4695
+ // Derive a human-readable goal from the first boss directive when run:start lacks one
4696
+ const firstBossComms = headEvents.find(e => e.type === 'org:comms' && (e.from === 'boss' || e.role === 'boss') && e.msg);
4697
+ const derivedGoal = first?.goal || firstBossComms?.msg?.slice(0, 80) || '';
4342
4698
  runs.push({ runId: f.replace('.jsonl', ''), startedAt: first?.ts || 0, endedAt: last?.ts || 0,
4343
4699
  status: last ? 'complete' : isStale ? 'stale' : 'running',
4344
- eventCount, cycleCount: cycles, goal: first?.goal || '', bossRole: first?.bossRole || '' });
4700
+ eventCount, cycleCount: cycles, goal: derivedGoal, bossRole: first?.bossRole || '' });
4345
4701
  } catch (_) {}
4346
4702
  }
4347
4703
  }
@@ -4373,114 +4729,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4373
4729
  // ------------------------------------------------- Mastermind event system
4374
4730
  // POST /api/mastermind/event — ingest event from mastermind skill
4375
4731
  if (req.method === 'POST' && url === '/api/mastermind/event') {
4376
- let body = '';
4377
- for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
4378
- let event = {};
4379
- try { event = JSON.parse(body); } catch (_) {}
4380
- event.ts = event.ts || Date.now();
4381
- // Use project path from event if provided (multi-project support).
4382
- // Security: path.isAbsolute() alone is insufficient — an attacker can
4383
- // supply event.project="/etc" and cause writes to system directories.
4384
- // Only accept paths that resolve to an existing directory AND are not
4385
- // the filesystem root (/), AND are not obviously system paths.
4386
- // Cap to 4096 chars to prevent OOM from huge path strings.
4387
- const _rawProject = event.project;
4388
- let eventProject = null;
4389
- if (typeof _rawProject === 'string' && _rawProject.length > 0 && _rawProject.length <= 4096
4390
- && path.isAbsolute(_rawProject)) {
4391
- // Reject filesystem root and common system directories
4392
- const _norm = path.resolve(_rawProject);
4393
- const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
4394
- if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
4395
- eventProject = _norm;
4396
- }
4397
- }
4398
- const root = eventProject || projectDir || process.cwd();
4399
- const dataDir = path.join(root, 'data');
4400
- try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
4401
- // Track known project dirs for aggregated session listing
4402
- if (eventProject) {
4403
- const knownFile = path.join(projectDir || process.cwd(), 'data', 'known-projects.json');
4404
- try {
4405
- let known = [];
4406
- try { known = JSON.parse(fs.readFileSync(knownFile, 'utf8')); } catch (_) {}
4407
- if (!known.includes(eventProject)) { known.push(eventProject); fs.writeFileSync(knownFile, JSON.stringify(known)); }
4408
- } catch (_) {}
4409
- }
4410
- try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
4411
- // Track active runs and route org events to run files
4412
- if (event.org) {
4413
- const _orgKey = String(event.org).trim();
4414
- // Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
4415
- if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
4416
- else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
4417
- if (event.type === 'run:complete' || event.type === 'org:complete') activeOrgRuns.delete(_orgKey);
4418
- }
4419
- // Persist to git-safe run file (survives branch switches + shared across worktrees)
4420
- if (event.org && event.runId) {
4421
- try {
4422
- const _orn = String(event.org).trim();
4423
- const _rid = String(event.runId).trim();
4424
- if (_orn.length > 0 && _orn.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_orn)
4425
- && _rid.length > 0 && _rid.length <= 80 && /^[a-z0-9][a-z0-9_-]*$/i.test(_rid)) {
4426
- const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
4427
- const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
4428
- fs.mkdirSync(_runDir, { recursive: true });
4429
- fs.appendFileSync(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
4430
- }
4431
- } catch (_) {}
4432
- }
4433
- // Persist session
4434
- try {
4435
- const sessFile = path.join(dataDir, 'mastermind-sessions.json');
4436
- let sessions = [];
4437
- try { sessions = JSON.parse(fs.readFileSync(sessFile, 'utf8')); } catch (_) {}
4438
- if (event.type === 'session:start') {
4439
- sessions.unshift({ id: event.session, ts: event.ts, prompt: event.prompt || '',
4440
- status: 'running', domains: [], events: [event], project: root });
4441
- } else {
4442
- const s = sessions.find(s => s.id === event.session);
4443
- if (s) {
4444
- (s.events = s.events || []).push(event);
4445
- if (event.type === 'domain:dispatch' && event.domain && !s.domains.includes(event.domain))
4446
- s.domains.push(event.domain);
4447
- if (event.type === 'session:complete') { s.status = event.status || 'complete'; s.endTs = event.ts; }
4448
- }
4449
- }
4450
- fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 50), null, 2));
4451
- // Also write individual session file for direct traceability.
4452
- // Security: validate event.session before using it as a filename to
4453
- // prevent path traversal (e.g. "../../../etc/cron.d/payload").
4454
- const sessionObj = sessions.find(s => s.id === event.session);
4455
- if (sessionObj) {
4456
- const sessDir = path.join(dataDir, 'sessions');
4457
- try { fs.mkdirSync(sessDir, { recursive: true }); } catch (_) {}
4458
- try {
4459
- const _sid = String(event.session || '').trim();
4460
- if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]+$/.test(_sid)) {
4461
- fs.writeFileSync(path.join(sessDir, `${_sid}.json`), JSON.stringify(sessionObj, null, 2));
4462
- }
4463
- } catch (_) {}
4464
- }
4465
- } catch (_) {}
4466
- // For org:stop events, write a stop marker the boss agent can detect
4467
- if (event.type === 'org:stop' && event.org) {
4468
- try {
4469
- const orgName = String(event.org).trim();
4470
- // Validate before any filesystem use — reject rather than strip
4471
- if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
4472
- const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
4473
- fs.mkdirSync(stopDir, { recursive: true });
4474
- fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
4475
- }
4476
- } catch (_) {}
4477
- }
4478
- // Broadcast to all mastermind SSE clients
4479
- const msg = `data: ${JSON.stringify(event)}\n\n`;
4480
- for (const c of mmSseClients) { try { c.write(msg); } catch (_) { mmSseClients.delete(c); } }
4481
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4482
- res.end('{"ok":true}');
4483
- return;
4732
+ return handleMastermindEvent(req, res);
4484
4733
  }
4485
4734
 
4486
4735
  // GET /api/mastermind-stream — SSE for real-time events
@@ -4510,6 +4759,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4510
4759
  try {
4511
4760
  const qp = new URL('http://x' + req.url).searchParams;
4512
4761
  const filterProject = qp.get('project');
4762
+ const limitParam = Math.min(parseInt(qp.get('limit') || '200', 10) || 200, 500);
4513
4763
  const serverRoot = projectDir || process.cwd();
4514
4764
  // Collect all project dirs to aggregate
4515
4765
  const projectDirs = new Set([serverRoot]);
@@ -4517,23 +4767,44 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4517
4767
  const known = JSON.parse(fs.readFileSync(path.join(serverRoot, 'data', 'known-projects.json'), 'utf8'));
4518
4768
  known.forEach(p => projectDirs.add(p));
4519
4769
  } catch (_) {}
4520
- // Load and merge sessions from all dirs
4521
4770
  let allSessions = [];
4522
4771
  for (const pd of projectDirs) {
4523
4772
  if (filterProject && pd !== filterProject) continue;
4524
- const f = path.join(pd, 'data', 'mastermind-sessions.json');
4525
- if (!fs.existsSync(f)) continue;
4526
- try {
4527
- const s = JSON.parse(fs.readFileSync(f, 'utf8'));
4528
- // Tag each session with its project if not already tagged
4529
- s.forEach(sess => { if (!sess.project) sess.project = pd; });
4530
- allSessions = allSessions.concat(s);
4531
- } catch (_) {}
4773
+ const sessDir = path.join(pd, 'data', 'sessions');
4774
+ const indexFile = path.join(sessDir, '_index.json');
4775
+ // ── New format: per-session JSONL + _index.json ──
4776
+ if (fs.existsSync(indexFile)) {
4777
+ try {
4778
+ const idx = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
4779
+ const top = idx.slice(0, limitParam);
4780
+ for (const entry of top) {
4781
+ const _sid = String(entry.id || '').trim();
4782
+ if (!_sid || !/^[a-zA-Z0-9_.-]+$/.test(_sid)) continue;
4783
+ let events = [];
4784
+ try {
4785
+ const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
4786
+ events = jl.trim().split('\n').filter(Boolean)
4787
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
4788
+ .filter(Boolean);
4789
+ } catch(_) {}
4790
+ allSessions.push({ ...entry, events, project: pd });
4791
+ }
4792
+ } catch(_) {}
4793
+ } else {
4794
+ // ── Legacy fallback: mastermind-sessions.json ──
4795
+ const f = path.join(pd, 'data', 'mastermind-sessions.json');
4796
+ if (fs.existsSync(f)) {
4797
+ try {
4798
+ const s = JSON.parse(fs.readFileSync(f, 'utf8'));
4799
+ s.forEach(sess => { if (!sess.project) sess.project = pd; });
4800
+ allSessions = allSessions.concat(s);
4801
+ } catch (_) {}
4802
+ }
4803
+ }
4532
4804
  }
4533
- // Sort by ts descending, cap at 100
4534
- allSessions.sort((a,b) => (b.ts||0)-(a.ts||0));
4805
+ allSessions.sort((a,b) => (b.ts||b.startedAt||0)-(a.ts||a.startedAt||0));
4535
4806
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4536
- res.end(JSON.stringify(allSessions.slice(0,100)));
4807
+ res.end(JSON.stringify(allSessions.slice(0, limitParam)));
4537
4808
  } catch (_) { res.writeHead(200); res.end('[]'); }
4538
4809
  return;
4539
4810
  }
@@ -4637,6 +4908,77 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4637
4908
  return;
4638
4909
  }
4639
4910
 
4911
+ // GET /api/status — live system snapshot for dashboard polling
4912
+ if (req.method === 'GET' && url === '/api/status') {
4913
+ try {
4914
+ const root = projectDir || process.cwd();
4915
+ // Active org runs: { orgName -> runId }
4916
+ const orgRuns = {};
4917
+ activeOrgRuns.forEach((runId, org) => { orgRuns[org] = runId; });
4918
+ // Recent events (last 10)
4919
+ let recentEvents = [];
4920
+ try {
4921
+ const evPath = path.join(root, 'data', 'mastermind-events.jsonl');
4922
+ const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-10);
4923
+ recentEvents = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
4924
+ } catch(_) {}
4925
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4926
+ res.end(JSON.stringify({
4927
+ ts: Date.now(),
4928
+ uptime: process.uptime(),
4929
+ dir: root,
4930
+ sseClients: mmSseClients.size,
4931
+ activeOrgs: Object.keys(orgRuns).length,
4932
+ orgRuns,
4933
+ recentEvents,
4934
+ }));
4935
+ } catch(err) {
4936
+ res.writeHead(500, { 'Content-Type': 'application/json' });
4937
+ res.end(JSON.stringify({ error: err.message }));
4938
+ }
4939
+ return;
4940
+ }
4941
+
4942
+ // GET /api/orgs/:name/runs/current — events from the active run file for an org
4943
+ if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/runs\/current$/.test(url)) {
4944
+ try {
4945
+ const orgName = decodeURIComponent(url.split('/')[3]);
4946
+ const _curQs = new URL(req.url, 'http://localhost').searchParams;
4947
+ const root = path.resolve(_curQs.get('dir') || projectDir || process.cwd());
4948
+ // Validate orgName
4949
+ if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
4950
+ res.writeHead(400); res.end('{"error":"invalid org name"}'); return;
4951
+ }
4952
+ const runId = activeOrgRuns.get(orgName);
4953
+ const monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
4954
+ // Try active run first, then fall back to most recent run file
4955
+ let runFile = null;
4956
+ if (runId) {
4957
+ const candidate = path.join(monoDir, 'orgs', orgName, 'runs', `${runId}.jsonl`);
4958
+ if (fs.existsSync(candidate)) runFile = candidate;
4959
+ }
4960
+ if (!runFile) {
4961
+ const runsDir = path.join(monoDir, 'orgs', orgName, 'runs');
4962
+ if (fs.existsSync(runsDir)) {
4963
+ const files = fs.readdirSync(runsDir).filter(f => f.endsWith('.jsonl'));
4964
+ if (files.length) {
4965
+ files.sort();
4966
+ runFile = path.join(runsDir, files[files.length - 1]);
4967
+ }
4968
+ }
4969
+ }
4970
+ if (!runFile) { res.writeHead(404); res.end('{"events":[],"runId":null}'); return; }
4971
+ const detectedRunId = path.basename(runFile, '.jsonl');
4972
+ const lines = fs.readFileSync(runFile, 'utf8').split('\n').filter(l => l.trim()).slice(-100);
4973
+ const events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
4974
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4975
+ res.end(JSON.stringify({ runId: detectedRunId, events, active: activeOrgRuns.has(orgName) }));
4976
+ } catch(err) {
4977
+ res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
4978
+ }
4979
+ return;
4980
+ }
4981
+
4640
4982
  // GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
4641
4983
  if (req.method === 'GET' && url === '/api/mastermind/metrics') {
4642
4984
  try {
@@ -4664,6 +5006,63 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
4664
5006
  const boundPort = await bindServer(server, port);
4665
5007
  const url = `http://localhost:${boundPort}`;
4666
5008
 
5009
+ // ── One-time migration: mastermind-sessions.json → per-session JSONL ─────
5010
+ // Runs once on startup. Existing sessions in the old monolithic format are
5011
+ // split into individual JSONL files + _index.json for O(1) event writes.
5012
+ try {
5013
+ const _migDataDir = path.join(projectDir || process.cwd(), 'data');
5014
+ const _migOldFile = path.join(_migDataDir, 'mastermind-sessions.json');
5015
+ const _migSessDir = path.join(_migDataDir, 'sessions');
5016
+ const _migIndexFile = path.join(_migSessDir, '_index.json');
5017
+ if (fs.existsSync(_migOldFile) && !fs.existsSync(_migIndexFile)) {
5018
+ try {
5019
+ const _migOld = JSON.parse(fs.readFileSync(_migOldFile, 'utf8'));
5020
+ fs.mkdirSync(_migSessDir, { recursive: true });
5021
+ const _migIndex = [];
5022
+ for (const sess of (_migOld || [])) {
5023
+ const _msid = String(sess.id || '').trim();
5024
+ if (!_msid || !/^[a-zA-Z0-9_.-]+$/.test(_msid)) continue;
5025
+ // Write per-session JSONL
5026
+ const _mEvts = (sess.events || []);
5027
+ const _mLines = _mEvts.map(e => JSON.stringify(e)).join('\n');
5028
+ fs.writeFileSync(path.join(_migSessDir, `${_msid}.jsonl`), _mLines + (_mLines ? '\n' : ''));
5029
+ _migIndex.push({ id: _msid, ts: sess.ts, prompt: sess.prompt || '',
5030
+ status: sess.status || 'complete', org: sess.org || '',
5031
+ startedAt: sess.ts || sess.startedAt, endedAt: sess.endTs || sess.endedAt,
5032
+ domains: sess.domains || [] });
5033
+ }
5034
+ fs.writeFileSync(_migIndexFile, JSON.stringify(_migIndex));
5035
+ console.log('[server] migrated ' + _migIndex.length + ' sessions to per-session JSONL format');
5036
+ } catch(_me) { console.warn('[server] session migration failed:', _me.message); }
5037
+ }
5038
+ } catch (_) {}
5039
+
5040
+ // Rebuild activeOrgRuns from disk so event enrichment (runId injection) still works
5041
+ // after a server restart. Without this, org events emitted mid-run that lack runId
5042
+ // are broadcast without it and _odtHandleLiveEvent drops them.
5043
+ try {
5044
+ const _rbOrgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
5045
+ if (fs.existsSync(_rbOrgsDir)) {
5046
+ for (const _rbOrg of fs.readdirSync(_rbOrgsDir)) {
5047
+ if (!_rbOrg || _rbOrg.startsWith('.') || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rbOrg)) continue;
5048
+ const _rbRunsDir = path.join(_rbOrgsDir, _rbOrg, 'runs');
5049
+ if (!fs.existsSync(_rbRunsDir)) continue;
5050
+ const _rbFiles = fs.readdirSync(_rbRunsDir)
5051
+ .filter(f => f.endsWith('.jsonl') && !f.endsWith('.convs.jsonl'))
5052
+ .sort().reverse();
5053
+ for (const _rbF of _rbFiles.slice(0, 5)) {
5054
+ try {
5055
+ const _rbId = _rbF.replace('.jsonl', '');
5056
+ const _rbContent = fs.readFileSync(path.join(_rbRunsDir, _rbF), 'utf8');
5057
+ const _rbLast = _rbContent.trim().split('\n').filter(Boolean).slice(-10);
5058
+ const _rbDone = _rbLast.some(l => { try { const e = JSON.parse(l); return e.type === 'run:complete' || e.type === 'org:complete'; } catch { return false; } });
5059
+ if (!_rbDone) { activeOrgRuns.set(_rbOrg, _rbId); break; }
5060
+ } catch (_) {}
5061
+ }
5062
+ }
5063
+ }
5064
+ } catch (_) {}
5065
+
4667
5066
  // ---------------------------------------------------------------- Watchers
4668
5067
  let debounceTimer = null;
4669
5068
  let pendingSections = new Set();