@monoes/monomindcli 1.9.17 → 1.10.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 (118) hide show
  1. package/.claude/commands/mastermind/_repeat.md +182 -39
  2. package/.claude/commands/mastermind/architect.md +17 -11
  3. package/.claude/commands/mastermind/brain.md +4 -0
  4. package/.claude/commands/mastermind/build.md +4 -0
  5. package/.claude/commands/mastermind/content.md +4 -0
  6. package/.claude/commands/mastermind/createorg.md +5 -3
  7. package/.claude/commands/mastermind/finance.md +4 -0
  8. package/.claude/commands/mastermind/idea.md +4 -0
  9. package/.claude/commands/mastermind/marketing.md +4 -0
  10. package/.claude/commands/mastermind/master.md +63 -37
  11. package/.claude/commands/mastermind/ops.md +4 -0
  12. package/.claude/commands/mastermind/release.md +4 -0
  13. package/.claude/commands/mastermind/research.md +4 -0
  14. package/.claude/commands/mastermind/review.md +4 -0
  15. package/.claude/commands/mastermind/runorg.md +5 -3
  16. package/.claude/commands/mastermind/sales.md +4 -0
  17. package/.claude/commands/mastermind/techport.md +9 -0
  18. package/.claude/commands/monomind/do.md +5 -1
  19. package/.claude/commands/monomind/idea.md +5 -1
  20. package/.claude/commands/monomind/improve.md +5 -1
  21. package/.claude/commands/monomind/repeat.md +85 -29
  22. package/.claude/commands/monomind/review.md +6 -2
  23. package/.claude/commands/monomind/understand.md +10 -8
  24. package/.claude/helpers/extras-registry.json +235 -235
  25. package/.claude/helpers/graphify-freshen.cjs +13 -1
  26. package/.claude/helpers/hook-handler.cjs +1 -1
  27. package/.claude/helpers/router.cjs +4 -1
  28. package/.claude/skills/mastermind/_protocol.md +28 -21
  29. package/.claude/skills/mastermind/access.md +236 -0
  30. package/.claude/skills/mastermind/activity.md +191 -0
  31. package/.claude/skills/mastermind/adapter-manager.md +259 -0
  32. package/.claude/skills/mastermind/adapters.md +204 -0
  33. package/.claude/skills/mastermind/agent-detail.md +242 -0
  34. package/.claude/skills/mastermind/agents.md +178 -0
  35. package/.claude/skills/mastermind/approval-detail.md +259 -0
  36. package/.claude/skills/mastermind/approve.md +181 -0
  37. package/.claude/skills/mastermind/architect.md +24 -8
  38. package/.claude/skills/mastermind/backup.md +197 -0
  39. package/.claude/skills/mastermind/bootstrap.md +190 -0
  40. package/.claude/skills/mastermind/budgets.md +237 -0
  41. package/.claude/skills/mastermind/companies.md +256 -0
  42. package/.claude/skills/mastermind/costs.md +151 -0
  43. package/.claude/skills/mastermind/createorg.md +23 -5
  44. package/.claude/skills/mastermind/diagnose.md +249 -0
  45. package/.claude/skills/mastermind/env.md +198 -0
  46. package/.claude/skills/mastermind/environments.md +250 -0
  47. package/.claude/skills/mastermind/export.md +324 -0
  48. package/.claude/skills/mastermind/goal-detail.md +255 -0
  49. package/.claude/skills/mastermind/goals.md +149 -0
  50. package/.claude/skills/mastermind/heartbeat.md +164 -0
  51. package/.claude/skills/mastermind/idea.md +250 -122
  52. package/.claude/skills/mastermind/import.md +281 -0
  53. package/.claude/skills/mastermind/inbox.md +214 -0
  54. package/.claude/skills/mastermind/instance-settings.md +315 -0
  55. package/.claude/skills/mastermind/instance.md +231 -0
  56. package/.claude/skills/mastermind/invite-landing.md +227 -0
  57. package/.claude/skills/mastermind/invites.md +254 -0
  58. package/.claude/skills/mastermind/issue-detail.md +291 -0
  59. package/.claude/skills/mastermind/issues.md +235 -0
  60. package/.claude/skills/mastermind/join-queue.md +170 -0
  61. package/.claude/skills/mastermind/liveness.md +392 -0
  62. package/.claude/skills/mastermind/memory.md +321 -0
  63. package/.claude/skills/mastermind/my-issues.md +146 -0
  64. package/.claude/skills/mastermind/new-agent.md +241 -0
  65. package/.claude/skills/mastermind/org-chart.md +207 -0
  66. package/.claude/skills/mastermind/org-settings.md +217 -0
  67. package/.claude/skills/mastermind/plan-to-tasks.md +136 -0
  68. package/.claude/skills/mastermind/plugin-manager.md +241 -0
  69. package/.claude/skills/mastermind/plugin-settings.md +273 -0
  70. package/.claude/skills/mastermind/plugins.md +190 -0
  71. package/.claude/skills/mastermind/profile.md +187 -0
  72. package/.claude/skills/mastermind/project-detail.md +249 -0
  73. package/.claude/skills/mastermind/project-workspace.md +244 -0
  74. package/.claude/skills/mastermind/projects.md +164 -0
  75. package/.claude/skills/mastermind/routine-detail.md +253 -0
  76. package/.claude/skills/mastermind/routines.md +202 -0
  77. package/.claude/skills/mastermind/runorg.md +74 -9
  78. package/.claude/skills/mastermind/search.md +186 -0
  79. package/.claude/skills/mastermind/secrets.md +199 -0
  80. package/.claude/skills/mastermind/skills.md +156 -0
  81. package/.claude/skills/mastermind/tasks.md +149 -0
  82. package/.claude/skills/mastermind/techport.md +5 -5
  83. package/.claude/skills/mastermind/threads.md +259 -0
  84. package/.claude/skills/mastermind/tree-control.md +250 -0
  85. package/.claude/skills/mastermind/wiki.md +314 -0
  86. package/.claude/skills/mastermind/workspace-detail.md +317 -0
  87. package/.claude/skills/mastermind/workspaces.md +261 -0
  88. package/.claude/skills/mastermind/worktree.md +187 -0
  89. package/dist/src/init/executor.js +8 -8
  90. package/dist/src/init/executor.js.map +1 -1
  91. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  92. package/dist/src/init/statusline-generator.js +12 -0
  93. package/dist/src/init/statusline-generator.js.map +1 -1
  94. package/dist/src/ui/.monomind/data/ranked-context.json +1 -1
  95. package/dist/src/ui/.monomind/loops/mastermind-review-1778664132789.json +16 -0
  96. package/dist/src/ui/.monomind/sessions/current.json +5 -5
  97. package/dist/src/ui/.monomind/sessions/session-1776778451399.json +15 -0
  98. package/dist/src/ui/dashboard.html +3030 -181
  99. package/dist/src/ui/data/mastermind-events.jsonl +8 -0
  100. package/dist/src/ui/data/mastermind-sessions.json +1 -0
  101. package/dist/src/ui/server.mjs +738 -0
  102. package/dist/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +1 -1
  104. package/.claude/skills/.monomind/data/ranked-context.json +0 -5
  105. package/.claude/skills/.monomind/sessions/current.json +0 -13
  106. package/.claude/skills/.monomind/sessions/session-1777829336455.json +0 -15
  107. package/.claude/skills/.monomind/sessions/session-1777831614725.json +0 -15
  108. package/.claude/skills/.monomind/sessions/session-1777832095857.json +0 -15
  109. package/.claude/skills/.monomind/sessions/session-1777839814183.json +0 -15
  110. package/.claude/skills/.monomind/sessions/session-1777841847131.json +0 -15
  111. package/.claude/skills/.monomind/sessions/session-1777843309463.json +0 -15
  112. package/.claude/skills/.monomind/sessions/session-1777880867159.json +0 -15
  113. package/.claude/skills/.monomind/sessions/session-1777881884593.json +0 -15
  114. package/.claude/skills/.monomind/sessions/session-1777884090471.json +0 -15
  115. package/.claude/skills/.monomind/sessions/session-1777884808221.json +0 -15
  116. package/.claude/skills/.monomind/sessions/session-1777885672155.json +0 -15
  117. package/.claude/skills/.monomind/sessions/session-1777886852818.json +0 -15
  118. package/.claude/skills/.monomind/sessions/session-1777896532690.json +0 -15
@@ -388,6 +388,56 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
388
388
  return;
389
389
  }
390
390
 
391
+ // ------------------------------------------------------- GET /api/adrs
392
+ if (req.method === 'GET' && url.startsWith('/api/adrs')) {
393
+ try {
394
+ const qs = new URL(req.url, 'http://localhost').searchParams;
395
+ const dir = qs.get('dir') || projectDir || process.cwd();
396
+ const d = path.resolve(dir || process.cwd());
397
+
398
+ const adrDirs = [
399
+ { path: path.join(d, 'docs', 'adrs'), group: 'all' },
400
+ ];
401
+
402
+ const adrs = [];
403
+ for (const { path: adrDir, group } of adrDirs) {
404
+ if (!fs.existsSync(adrDir)) continue;
405
+ const files = fs.readdirSync(adrDir).filter(f => f.endsWith('.md') && f !== 'README.md' && f !== 'v3-adrs.md' && f !== 'SECURITY-REVIEW-SUMMARY.md');
406
+ for (const fname of files.sort()) {
407
+ const resolvedGroup = /^ADR-G/i.test(fname) ? 'guidance' : 'implementation';
408
+ try {
409
+ const raw = fs.readFileSync(path.join(adrDir, fname), 'utf8');
410
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
411
+ const header = raw.split('\n').slice(0, 20).join('\n');
412
+ const statusTableMatch = header.match(/^\|\s*\*{0,2}Status\*{0,2}\s*\|\s*\*{0,2}([^|*\n]{2,40}?)\*{0,2}\s*\|/im);
413
+ const statusInlineMatch = header.match(/\*\*Status[:\s]+\*?\*?\s*(Accepted|Implemented|Proposed|Superseded|Deprecated|Draft|Rejected|Complete|Active|Retired)[^*]*/i);
414
+ const statusMatch = statusTableMatch || statusInlineMatch;
415
+ const dateInlineMatch = header.match(/\*\*Date[:\s]+\*?\*?\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i);
416
+ const dateMatch = raw.match(/\|\s*\*{0,2}Date\*{0,2}\s*\|\s*\*{0,2}([^|*\n]+?)\*{0,2}\s*\|/i) || dateInlineMatch || raw.match(/Date[:\s]+([0-9]{4}-[0-9]{2}-[0-9]{2})/);
417
+ const numMatch = fname.match(/ADR-([A-Z]*[0-9]+)/i);
418
+ const summaryMatch = raw.match(/##\s+(?:Context|Summary|Problem Statement)[^\n]*\n+([\s\S]{20,300})/i);
419
+ adrs.push({
420
+ number: numMatch ? 'ADR-' + numMatch[1] : fname.replace('.md', ''),
421
+ title: titleMatch ? titleMatch[1].replace(/^ADR-[A-Z0-9-]+[:\s]+/i, '').trim() : fname.replace('.md', ''),
422
+ status: statusMatch ? statusMatch[1].trim() : 'Unknown',
423
+ date: dateMatch ? dateMatch[1].trim() : null,
424
+ summary: summaryMatch ? summaryMatch[1].replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() : null,
425
+ group: resolvedGroup,
426
+ file: fname,
427
+ });
428
+ } catch { /* skip unreadable */ }
429
+ }
430
+ }
431
+
432
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
433
+ res.end(JSON.stringify({ adrs }));
434
+ } catch (err) {
435
+ res.writeHead(500, { 'Content-Type': 'application/json' });
436
+ res.end(JSON.stringify({ error: err.message }));
437
+ }
438
+ return;
439
+ }
440
+
391
441
  // ------------------------------------------------------- GET /api/memory-files
392
442
  if (req.method === 'GET' && url === '/api/memory-files') {
393
443
  try {
@@ -2138,6 +2188,655 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2138
2188
  return;
2139
2189
  }
2140
2190
 
2191
+ // GET /api/org/:name — ORG ROOM: rich org data (config + state + tasks + routines + goals)
2192
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
2193
+ try {
2194
+ const orgName = decodeURIComponent(url.slice('/api/org/'.length));
2195
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2196
+ const d = projectDir || process.cwd();
2197
+ const orgsDir = path.join(d, '.monomind', 'orgs');
2198
+
2199
+ const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
2200
+
2201
+ const configFile = path.join(orgsDir, `${orgName}.json`);
2202
+ if (!fs.existsSync(configFile)) { res.writeHead(404); res.end('{"error":"org not found"}'); return; }
2203
+ const config = readJsonSafe(configFile);
2204
+
2205
+ const state = readJsonSafe(path.join(orgsDir, `${orgName}-state.json`)) || { agents: {} };
2206
+ const goalsData = readJsonSafe(path.join(orgsDir, `${orgName}-goals.json`)) || { goals: [] };
2207
+ const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || { routines: [] };
2208
+ const approvalsData = readJsonSafe(path.join(orgsDir, `${orgName}-approvals.json`)) || { approvals: [] };
2209
+
2210
+ // Check running status from stop file absence + state
2211
+ const stopFile = path.join(orgsDir, '.stops', `${orgName}.stop`);
2212
+ const running = !fs.existsSync(stopFile) && Object.values(state.agents || {}).some(a => a.status === 'running');
2213
+
2214
+ const result = { config, state, goals: goalsData.goals, routines: routinesData.routines,
2215
+ approvals: approvalsData.approvals, running, tasks: { todo: [], doing: [], done: [] } };
2216
+
2217
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2218
+ res.end(JSON.stringify(result));
2219
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2220
+ return;
2221
+ }
2222
+
2223
+ // GET /api/org/:name/activity — recent org events from mastermind-events.jsonl
2224
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/activity$/i.test(url)) {
2225
+ try {
2226
+ const parts = url.split('/');
2227
+ const orgName = decodeURIComponent(parts[3]);
2228
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
2229
+ const d = projectDir || process.cwd();
2230
+ const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
2231
+ let events = [];
2232
+ if (fs.existsSync(eventsFile)) {
2233
+ const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean);
2234
+ events = lines.slice(-200).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean)
2235
+ .filter(e => e.org === orgName || e.session && typeof e.org === 'undefined')
2236
+ .filter(e => e.org === orgName)
2237
+ .reverse().slice(0, 100);
2238
+ }
2239
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2240
+ res.end(JSON.stringify(events));
2241
+ } catch(_) { res.writeHead(500); res.end('[]'); }
2242
+ return;
2243
+ }
2244
+
2245
+ // GET /api/org/:name/projects — org projects from projects json file
2246
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/projects$/i.test(url)) {
2247
+ try {
2248
+ const parts = url.split('/');
2249
+ const orgName = decodeURIComponent(parts[3]);
2250
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
2251
+ const d = projectDir || process.cwd();
2252
+ const projFile = path.join(d, '.monomind', 'orgs', `${orgName}-projects.json`);
2253
+ if (!fs.existsSync(projFile)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('[]'); return; }
2254
+ const data = JSON.parse(fs.readFileSync(projFile, 'utf8'));
2255
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2256
+ res.end(JSON.stringify(data.projects || []));
2257
+ } catch(_) { res.writeHead(500); res.end('[]'); }
2258
+ return;
2259
+ }
2260
+
2261
+ // GET /api/org/:name/members — org member list and join requests
2262
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/members$/i.test(url)) {
2263
+ try {
2264
+ const parts = url.split('/');
2265
+ const orgName = decodeURIComponent(parts[3]);
2266
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
2267
+ const d = projectDir || process.cwd();
2268
+ const membersFile = path.join(d, '.monomind', 'orgs', `${orgName}-members.json`);
2269
+ if (!fs.existsSync(membersFile)) {
2270
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2271
+ res.end('{"members":[],"join_requests":[]}');
2272
+ return;
2273
+ }
2274
+ const data = JSON.parse(fs.readFileSync(membersFile, 'utf8'));
2275
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2276
+ res.end(JSON.stringify(data));
2277
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2278
+ return;
2279
+ }
2280
+
2281
+ // GET /api/org/:name/adapters — org adapter registry
2282
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/adapters$/i.test(url)) {
2283
+ try {
2284
+ const parts = url.split('/');
2285
+ const orgName = decodeURIComponent(parts[3]);
2286
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
2287
+ const d = projectDir || process.cwd();
2288
+ const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
2289
+ if (!fs.existsSync(adaptersFile)) {
2290
+ // Return defaults derived from org config if available
2291
+ const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
2292
+ let defaultAdapter = 'claude-sonnet-4-6';
2293
+ try { defaultAdapter = JSON.parse(fs.readFileSync(orgFile, 'utf8'))?.run_config?.ceo_adapter || defaultAdapter; } catch(_) {}
2294
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2295
+ res.end(JSON.stringify({ default_adapter: defaultAdapter, adapters: [
2296
+ { type: 'claude-local', label: 'Claude (local CLI)', source: 'built-in', disabled: false, modelsCount: 3 },
2297
+ { type: 'gemini-local', label: 'Gemini (local)', source: 'built-in', disabled: false, modelsCount: 1 },
2298
+ { type: 'http', label: 'HTTP Adapter', source: 'built-in', disabled: true, modelsCount: 0 },
2299
+ ]}));
2300
+ return;
2301
+ }
2302
+ const data = JSON.parse(fs.readFileSync(adaptersFile, 'utf8'));
2303
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2304
+ res.end(JSON.stringify(data));
2305
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2306
+ return;
2307
+ }
2308
+
2309
+ // GET /api/org/:name/skills — list skills from .claude/skills/ mapped to org roles
2310
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/skills$/i.test(url)) {
2311
+ try {
2312
+ const parts = url.split('/');
2313
+ const orgName = decodeURIComponent(parts[3]);
2314
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
2315
+ const d = projectDir || process.cwd();
2316
+ const skillsDir = path.join(d, '.claude', 'skills');
2317
+ const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
2318
+
2319
+ // Scan skills directory
2320
+ const skills = [];
2321
+ if (fs.existsSync(skillsDir)) {
2322
+ const scanDir = (dir, prefix) => {
2323
+ try {
2324
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2325
+ if (entry.isDirectory()) { scanDir(path.join(dir, entry.name), `${entry.name}:`); }
2326
+ else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
2327
+ const slug = entry.name.replace(/\.md$/, '');
2328
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf8').slice(0, 500);
2329
+ const typeMatch = content.match(/^type:\s*(.+)$/m);
2330
+ const modeMatch = content.match(/^default_mode:\s*(.+)$/m);
2331
+ const descMatch = content.match(/^description:\s*(.+)$/m);
2332
+ skills.push({
2333
+ name: `${prefix}${slug}`,
2334
+ slug,
2335
+ type: typeMatch ? typeMatch[1].trim() : 'skill',
2336
+ default_mode: modeMatch ? modeMatch[1].trim() : 'auto',
2337
+ description: descMatch ? descMatch[1].trim() : '',
2338
+ });
2339
+ }
2340
+ }
2341
+ } catch(_) {}
2342
+ };
2343
+ scanDir(skillsDir, '');
2344
+ }
2345
+
2346
+ // Map skills enabled per role from org config
2347
+ let roleSkillMap = {};
2348
+ if (fs.existsSync(orgFile)) {
2349
+ try {
2350
+ const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
2351
+ for (const role of (config.roles || [])) {
2352
+ roleSkillMap[role.id] = role.skills || [];
2353
+ }
2354
+ } catch(_) {}
2355
+ }
2356
+
2357
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2358
+ res.end(JSON.stringify({ skills, role_skill_map: roleSkillMap }));
2359
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2360
+ return;
2361
+ }
2362
+
2363
+ // GET /api/org/:name/search?q=<query> — fuzzy search across org data
2364
+ if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/search(\?.*)?$/i.test(url)) {
2365
+ try {
2366
+ const urlObj = new URL(`http://x${url}`);
2367
+ const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
2368
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
2369
+ const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
2370
+ if (!q || q.length < 2) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('{"hits":[]}'); return; }
2371
+
2372
+ const d = projectDir || process.cwd();
2373
+ const orgsDir = path.join(d, '.monomind', 'orgs');
2374
+ const readJ = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
2375
+
2376
+ const hits = [];
2377
+ const match = (str) => str && str.toLowerCase().includes(q);
2378
+
2379
+ // Agents
2380
+ const config = readJ(path.join(orgsDir, `${orgName}.json`));
2381
+ for (const role of (config?.roles || [])) {
2382
+ if (match(role.id) || match(role.title) || (role.responsibilities || []).some(r => match(r))) {
2383
+ hits.push({ type: 'agent', id: role.id, title: role.title, meta: role.agent_type });
2384
+ }
2385
+ }
2386
+
2387
+ // Goals
2388
+ const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
2389
+ for (const g of (goals?.goals || [])) {
2390
+ if (match(g.title) || match(g.description)) {
2391
+ hits.push({ type: 'goal', id: g.id, title: g.title, meta: g.status || 'open' });
2392
+ }
2393
+ }
2394
+
2395
+ // Routines
2396
+ const routines = readJ(path.join(orgsDir, `${orgName}-routines.json`));
2397
+ for (const r of (routines?.routines || [])) {
2398
+ if (match(r.name) || match(r.description)) {
2399
+ hits.push({ type: 'routine', id: r.name, title: r.name, meta: r.schedule || '' });
2400
+ }
2401
+ }
2402
+
2403
+ // Approvals
2404
+ const approvals = readJ(path.join(orgsDir, `${orgName}-approvals.json`));
2405
+ for (const a of (approvals?.approvals || [])) {
2406
+ if (match(a.title) || match(a.action) || match(a.agent_id)) {
2407
+ hits.push({ type: 'approval', id: a.id, title: a.title, meta: a.status });
2408
+ }
2409
+ }
2410
+
2411
+ // Projects
2412
+ const projects = readJ(path.join(orgsDir, `${orgName}-projects.json`));
2413
+ for (const p of (projects?.projects || [])) {
2414
+ if (match(p.name) || match(p.description)) {
2415
+ hits.push({ type: 'project', id: p.id || p.name, title: p.name, meta: p.status || 'active' });
2416
+ }
2417
+ }
2418
+
2419
+ // Recent activity events
2420
+ const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
2421
+ if (fs.existsSync(eventsFile)) {
2422
+ const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).slice(-500);
2423
+ for (const l of lines) {
2424
+ try {
2425
+ const e = JSON.parse(l);
2426
+ if (e.org === orgName && match(JSON.stringify(e))) {
2427
+ hits.push({ type: 'event', id: String(e.ts), title: e.type, meta: e.role || e.task || '' });
2428
+ if (hits.length >= 50) break;
2429
+ }
2430
+ } catch(_) {}
2431
+ }
2432
+ }
2433
+
2434
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2435
+ res.end(JSON.stringify({ q, hits: hits.slice(0, 50) }));
2436
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2437
+ return;
2438
+ }
2439
+
2440
+ // GET /api/org/:name/issues — org task/issue list from issues file
2441
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/issues$/i)) {
2442
+ try {
2443
+ const orgName = decodeURIComponent(url.split('/')[3]);
2444
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2445
+ const issuesPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-issues.json`);
2446
+ let payload = { issues: [] };
2447
+ try {
2448
+ const raw = JSON.parse(fs.readFileSync(issuesPath, 'utf8'));
2449
+ payload.issues = (raw.issues || []).map(i => ({
2450
+ id: i.id, slug: i.slug, title: i.title, status: i.status || 'open',
2451
+ priority: i.priority || 'medium', assignee_id: i.assignee_id || null,
2452
+ project_id: i.project_id || null, parent_id: i.parent_id || null,
2453
+ created_at: i.created_at, updated_at: i.updated_at
2454
+ }));
2455
+ } catch(_) { /* file missing is fine */ }
2456
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2457
+ res.end(JSON.stringify(payload));
2458
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2459
+ return;
2460
+ }
2461
+
2462
+ // GET /api/org/:name/health — aggregate org health metrics
2463
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/health$/i)) {
2464
+ try {
2465
+ const orgName = decodeURIComponent(url.split('/')[3]);
2466
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2467
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2468
+
2469
+ let agentsRunning = 0, agentsIdle = 0, openIssues = 0, inProgressIssues = 0;
2470
+ let budgetUsedTokens = 0, budgetMaxTokens = 0;
2471
+ let successRuns = 0, totalRuns = 0;
2472
+
2473
+ // State: agent statuses
2474
+ try {
2475
+ const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
2476
+ const agents = state.agents || {};
2477
+ Object.values(agents).forEach(a => {
2478
+ if (a.status === 'running') agentsRunning++;
2479
+ else agentsIdle++;
2480
+ budgetUsedTokens += (a.tokens_used || 0);
2481
+ });
2482
+ } catch(_) {}
2483
+
2484
+ // Budget cap from org config
2485
+ try {
2486
+ const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
2487
+ budgetMaxTokens = cfg.run_config?.budget_tokens || cfg.budget_tokens || 0;
2488
+ } catch(_) {}
2489
+
2490
+ // Issues: open count
2491
+ try {
2492
+ const iss = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
2493
+ openIssues = (iss.issues || []).filter(i => i.status === 'open').length;
2494
+ inProgressIssues = (iss.issues || []).filter(i => i.status === 'in_progress').length;
2495
+ } catch(_) {}
2496
+
2497
+ // Activity: 7-day success rate
2498
+ try {
2499
+ const actPath = path.join(base, `${orgName}-activity.jsonl`);
2500
+ const lines = fs.readFileSync(actPath, 'utf8').split('\n').filter(Boolean);
2501
+ const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
2502
+ lines.forEach(line => {
2503
+ try {
2504
+ const ev = JSON.parse(line);
2505
+ if (!ev.ts || ev.ts < cutoff) return;
2506
+ totalRuns++;
2507
+ if (ev.type && ev.type.includes('complete')) successRuns++;
2508
+ } catch(_) {}
2509
+ });
2510
+ } catch(_) {}
2511
+
2512
+ const budgetUsedPct = budgetMaxTokens > 0 ? Math.round((budgetUsedTokens / budgetMaxTokens) * 100) : null;
2513
+ const successRate = totalRuns > 0 ? Math.round((successRuns / totalRuns) * 100) : null;
2514
+
2515
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2516
+ res.end(JSON.stringify({
2517
+ agents_running: agentsRunning,
2518
+ agents_idle: agentsIdle,
2519
+ open_issues: openIssues,
2520
+ in_progress_issues: inProgressIssues,
2521
+ budget_used_tokens: budgetUsedTokens,
2522
+ budget_max_tokens: budgetMaxTokens,
2523
+ budget_used_pct: budgetUsedPct,
2524
+ run_success_rate_7d: successRate,
2525
+ total_runs_7d: totalRuns,
2526
+ }));
2527
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2528
+ return;
2529
+ }
2530
+
2531
+ // GET /api/org/:name/environments — org execution environments (strips key material)
2532
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/environments$/i)) {
2533
+ try {
2534
+ const orgName = decodeURIComponent(url.split('/')[3]);
2535
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2536
+ const envsPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-environments.json`);
2537
+ let payload = { environments: [], default_env: null };
2538
+ try {
2539
+ const raw = JSON.parse(fs.readFileSync(envsPath, 'utf8'));
2540
+ // Strip any accidental key_material or private_key fields — never send to browser
2541
+ payload.default_env = raw.default_env || null;
2542
+ payload.environments = (raw.environments || []).map(e => {
2543
+ const safe = { ...e };
2544
+ delete safe.key_material;
2545
+ delete safe.private_key;
2546
+ delete safe.ssh_key;
2547
+ delete safe.password;
2548
+ return safe;
2549
+ });
2550
+ } catch(_) { /* file missing is fine — return empty */ }
2551
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2552
+ res.end(JSON.stringify(payload));
2553
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2554
+ return;
2555
+ }
2556
+
2557
+ // GET /api/org/:name/workspaces — org workspaces cross-referenced with worktree registry
2558
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/workspaces$/i)) {
2559
+ try {
2560
+ const orgName = decodeURIComponent(url.split('/')[3]);
2561
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2562
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2563
+ let payload = { workspaces: [] };
2564
+ try {
2565
+ const wsRaw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-workspaces.json`), 'utf8'));
2566
+ const workspaces = wsRaw.workspaces || [];
2567
+ // Optionally cross-reference worktree registry for branch/status enrichment
2568
+ let worktreeMap = {};
2569
+ try {
2570
+ const wtRaw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-worktrees.json`), 'utf8'));
2571
+ (wtRaw.worktrees || []).forEach(wt => { worktreeMap[wt.path] = wt; });
2572
+ } catch(_) { /* no worktree registry, that's fine */ }
2573
+ payload.workspaces = workspaces.map(w => {
2574
+ const wt = w.worktree_path ? worktreeMap[w.worktree_path] : null;
2575
+ return wt ? { ...w, branch: w.branch || wt.branch || w.branch } : w;
2576
+ });
2577
+ } catch(_) { /* file missing is fine */ }
2578
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2579
+ res.end(JSON.stringify(payload));
2580
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2581
+ return;
2582
+ }
2583
+
2584
+ // GET /api/org/:name/invites — active invites + pending join requests
2585
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/invites$/i)) {
2586
+ try {
2587
+ const orgName = decodeURIComponent(url.split('/')[3]);
2588
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2589
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2590
+ let payload = { invites: [], join_requests: [] };
2591
+ try {
2592
+ const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-members.json`), 'utf8'));
2593
+ const all = raw.join_requests || [];
2594
+ payload.invites = all.filter(r => r.type === 'invite' && r.status === 'pending')
2595
+ .map(r => ({ id: r.id, token: r.token ? r.token.slice(0, 8) + '…' : r.id, role: r.role || 'operator', createdAt: r.createdAt || null, status: r.status }));
2596
+ payload.join_requests = all.filter(r => r.type !== 'invite' && r.status === 'pending_approval')
2597
+ .map(r => ({ id: r.id, requestType: r.requestType || 'human', role: r.role || 'viewer', createdAt: r.createdAt || null, message: r.message || '' }));
2598
+ } catch(_) { /* members file missing */ }
2599
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2600
+ res.end(JSON.stringify(payload));
2601
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2602
+ return;
2603
+ }
2604
+
2605
+ // GET /api/org/:name/plugins — plugins from registry filtered/merged with org overrides
2606
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/plugins$/i)) {
2607
+ try {
2608
+ const orgName = decodeURIComponent(url.split('/')[3]);
2609
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2610
+ const base = path.join(projectDir || process.cwd(), '.monomind');
2611
+ let plugins = [];
2612
+ try {
2613
+ const reg = JSON.parse(fs.readFileSync(path.join(base, 'plugins', 'registry.json'), 'utf8'));
2614
+ plugins = reg.plugins || [];
2615
+ // Strip sensitive config fields from output
2616
+ plugins = plugins.map(p => {
2617
+ const safe = { ...p };
2618
+ if (safe.config) {
2619
+ safe.config = Object.fromEntries(
2620
+ Object.entries(safe.config).map(([k, v]) =>
2621
+ (/key|token|secret|password|api/i.test(k) ? [k, '***'] : [k, v])
2622
+ )
2623
+ );
2624
+ }
2625
+ return safe;
2626
+ });
2627
+ } catch(_) { /* no global registry */ }
2628
+ // Merge org-level overrides
2629
+ try {
2630
+ const orgPlugins = JSON.parse(fs.readFileSync(path.join(base, 'orgs', `${orgName}-plugins.json`), 'utf8'));
2631
+ const overrideMap = {};
2632
+ (orgPlugins.plugins || []).forEach(p => { overrideMap[p.id] = p; });
2633
+ if (Object.keys(overrideMap).length) {
2634
+ plugins = plugins.map(p => overrideMap[p.id] ? { ...p, ...overrideMap[p.id], _orgOverride: true } : p);
2635
+ }
2636
+ } catch(_) { /* no org-level overrides */ }
2637
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2638
+ res.end(JSON.stringify({ plugins }));
2639
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2640
+ return;
2641
+ }
2642
+
2643
+ // GET /api/org/:name/my-issues — open + in_progress issues (self-assignable queue)
2644
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/my-issues$/i)) {
2645
+ try {
2646
+ const orgName = decodeURIComponent(url.split('/')[3]);
2647
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2648
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2649
+ let payload = { issues: [] };
2650
+ try {
2651
+ const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
2652
+ // Return open + in_progress issues — the "my issues" queue for the operator
2653
+ payload.issues = (raw.issues || [])
2654
+ .filter(i => i.status === 'open' || i.status === 'in_progress')
2655
+ .map(i => ({
2656
+ id: i.id,
2657
+ title: i.title || null,
2658
+ status: i.status || 'open',
2659
+ priority: i.priority || 'medium',
2660
+ assigneeId: i.assigneeId || i.assigned_to || null,
2661
+ projectId: i.projectId || i.project_id || null,
2662
+ createdAt: i.createdAt || null,
2663
+ lastActivityAt: i.lastActivityAt || null,
2664
+ }));
2665
+ } catch(_) { /* issues file missing */ }
2666
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2667
+ res.end(JSON.stringify(payload));
2668
+ } catch(_) { res.writeHead(500); res.end('{}'); }
2669
+ return;
2670
+ }
2671
+
2672
+ // GET /api/org/:name/agents — agents from roles + merged heartbeat state
2673
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agents$/i)) {
2674
+ try {
2675
+ const orgName = decodeURIComponent(url.split('/')[3]);
2676
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2677
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2678
+ const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
2679
+ const config = readJsonSafe(path.join(base, `${orgName}.json`)) || {};
2680
+ const stateData = readJsonSafe(path.join(base, `${orgName}-state.json`)) || {};
2681
+ const agentState = stateData.agents || stateData.roles
2682
+ ? (stateData.agents || Object.fromEntries((stateData.roles||[]).map(r => [r.id, r])))
2683
+ : {};
2684
+ const roles = config.roles || [];
2685
+ const agents = roles.map(r => {
2686
+ const s = agentState[r.id] || {};
2687
+ return {
2688
+ id: r.id,
2689
+ title: r.title || r.id,
2690
+ adapterType: (r.adapter && r.adapter.type) || null,
2691
+ adapterModel: (r.adapter && r.adapter.model) || null,
2692
+ governance: r.governance || null,
2693
+ reportsTo: r.reports_to || null,
2694
+ status: s.status || 'idle',
2695
+ lastHeartbeat: s.last_heartbeat || s.lastHeartbeat || null,
2696
+ tokensIn: s.tokens_in || 0,
2697
+ tokensOut: s.tokens_out || 0,
2698
+ skills: r.skills || [],
2699
+ };
2700
+ });
2701
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2702
+ res.end(JSON.stringify({ agents }));
2703
+ } catch(_) { res.writeHead(500); res.end('{"agents":[]}'); }
2704
+ return;
2705
+ }
2706
+
2707
+ // GET /api/org/:name/approvals — full approvals list with status filter support
2708
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals(\?.*)?$/i)) {
2709
+ try {
2710
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
2711
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2712
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2713
+ const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
2714
+ const data = readJsonSafe(path.join(base, `${orgName}-approvals.json`)) || { approvals: [] };
2715
+ const approvals = (data.approvals || [])
2716
+ .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0))
2717
+ .map(a => ({
2718
+ id: a.id,
2719
+ title: a.title || a.action || null,
2720
+ status: a.status || 'pending',
2721
+ agentId: a.agentId || a.agent_id || null,
2722
+ agentTitle: a.agentTitle || null,
2723
+ payload: a.payload || null,
2724
+ createdAt: a.createdAt || null,
2725
+ updatedAt: a.updatedAt || null,
2726
+ resolvedAt: a.resolvedAt || null,
2727
+ resolvedBy: a.resolvedBy || null,
2728
+ }));
2729
+ const pending = approvals.filter(a => a.status === 'pending' || a.status === 'revision_requested').length;
2730
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2731
+ res.end(JSON.stringify({ approvals, pending }));
2732
+ } catch(_) { res.writeHead(500); res.end('{"approvals":[],"pending":0}'); }
2733
+ return;
2734
+ }
2735
+
2736
+ // GET /api/org/:name/secrets — masked secrets list (NEVER exposes values)
2737
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/secrets$/i)) {
2738
+ try {
2739
+ const orgName = decodeURIComponent(url.split('/')[3]);
2740
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2741
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2742
+ const secretsDir = path.join(base, '.secrets');
2743
+ const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
2744
+ // Read secrets index — NEVER expose actual values
2745
+ const indexFile = path.join(secretsDir, `${orgName}-index.json`);
2746
+ const data = readJsonSafe(indexFile) || { secrets: [] };
2747
+ const secrets = (data.secrets || []).map(s => ({
2748
+ name: s.name,
2749
+ maskedRef: s.maskedRef || `${(s.name||'').substring(0,4)}***`,
2750
+ status: s.status || 'active',
2751
+ createdAt: s.createdAt || null,
2752
+ rotatedAt: s.rotatedAt || null,
2753
+ lastUsedAt: s.lastUsedAt || null,
2754
+ usageCount: s.usageCount || 0,
2755
+ }));
2756
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2757
+ res.end(JSON.stringify({ secrets }));
2758
+ } catch(_) { res.writeHead(500); res.end('{"secrets":[]}'); }
2759
+ return;
2760
+ }
2761
+
2762
+ // GET /api/org/:name/budgets — org and per-agent budget data
2763
+ // Returns: { org_budget: {limit_tokens, limit_usd}, agent_budgets: {agentId: {limit_usd}}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
2764
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/budgets$/i)) {
2765
+ try {
2766
+ const orgName = decodeURIComponent(url.split('/')[3]);
2767
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2768
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
2769
+ let budgetData = { org_budget: {}, agent_budgets: {}, period: 'monthly', currency: 'USD' };
2770
+ try { budgetData = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-budgets.json`), 'utf8')); } catch(_) {}
2771
+ // Enrich with per-agent spend from state file
2772
+ let agents = [];
2773
+ try {
2774
+ const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
2775
+ agents = (state.roles || []).map(r => ({
2776
+ id: r.id, title: r.title,
2777
+ tokens_in: r.tokens_in || 0, tokens_out: r.tokens_out || 0, total_cost_usd: r.total_cost_usd || 0
2778
+ }));
2779
+ } catch(_) {}
2780
+ // Also include roles from org config if state is empty
2781
+ if (!agents.length) {
2782
+ try {
2783
+ const org = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
2784
+ agents = (org.roles || []).map(r => ({ id: r.id, title: r.title, tokens_in: 0, tokens_out: 0, total_cost_usd: 0 }));
2785
+ } catch(_) {}
2786
+ }
2787
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2788
+ res.end(JSON.stringify({ ...budgetData, agents }));
2789
+ } catch(_) { res.writeHead(500); res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}'); }
2790
+ return;
2791
+ }
2792
+
2793
+ // GET /api/org/:name/threads — conversation threads from threads.jsonl
2794
+ // Returns: { threads: [{id, subject, authorId, authorName, issueId, createdAt, messages:[]}] }
2795
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/threads$/i)) {
2796
+ try {
2797
+ const orgName = decodeURIComponent(url.split('/')[3]);
2798
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2799
+ const threadsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-threads.jsonl`);
2800
+ let threads = [];
2801
+ try {
2802
+ const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
2803
+ threads = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
2804
+ threads = threads.filter(t => t.type === 'thread' || !t.type);
2805
+ } catch(_) {}
2806
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2807
+ res.end(JSON.stringify({ threads }));
2808
+ } catch(_) { res.writeHead(500); res.end('{"threads":[]}'); }
2809
+ return;
2810
+ }
2811
+
2812
+ // GET /api/org/:name/join-requests — pending join requests for this org
2813
+ // Returns: { requests: [{id, requesterId, requesterName, type, status, createdAt, resolvedAt}], pending: N }
2814
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/join-requests(\?.*)?$/i)) {
2815
+ try {
2816
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
2817
+ if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
2818
+ const joinFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-join-requests.json`);
2819
+ let requests = [];
2820
+ try {
2821
+ const raw = fs.readFileSync(joinFile, 'utf8');
2822
+ const data = JSON.parse(raw);
2823
+ requests = (data.requests || []).map(r => ({
2824
+ id: r.id,
2825
+ requesterId: r.requesterId,
2826
+ requesterName: r.requesterName || r.requesterId,
2827
+ type: r.type || 'human',
2828
+ status: r.status || 'pending_approval',
2829
+ createdAt: r.createdAt,
2830
+ resolvedAt: r.resolvedAt || null,
2831
+ }));
2832
+ } catch(_) {}
2833
+ const pending = requests.filter(r => r.status === 'pending_approval').length;
2834
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2835
+ res.end(JSON.stringify({ requests, pending }));
2836
+ } catch(_) { res.writeHead(500); res.end('{"requests":[],"pending":0}'); }
2837
+ return;
2838
+ }
2839
+
2141
2840
  // POST /api/orgs/:name/stop — send stop signal to a running org
2142
2841
  if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/stop$/i)) {
2143
2842
  try {
@@ -2349,6 +3048,45 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2349
3048
  return;
2350
3049
  }
2351
3050
 
3051
+ // GET /api/mastermind/loops — list all active loop state files
3052
+ if (req.method === 'GET' && url === '/api/mastermind/loops') {
3053
+ try {
3054
+ const loopsDir = path.join(projectDir || process.cwd(), '.monomind', 'loops');
3055
+ const loops = [];
3056
+ if (fs.existsSync(loopsDir)) {
3057
+ const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('.json') && !f.includes('-hil'));
3058
+ for (const f of files) {
3059
+ try {
3060
+ const d = JSON.parse(fs.readFileSync(path.join(loopsDir, f), 'utf8'));
3061
+ loops.push(d);
3062
+ } catch(_) {}
3063
+ }
3064
+ }
3065
+ loops.sort((a, b) => (b.lastRunAt || 0) - (a.lastRunAt || 0));
3066
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3067
+ res.end(JSON.stringify({ loops }));
3068
+ } catch(_) { res.writeHead(500); res.end('{"loops":[]}'); }
3069
+ return;
3070
+ }
3071
+
3072
+ // GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
3073
+ if (req.method === 'GET' && url === '/api/mastermind/metrics') {
3074
+ try {
3075
+ const base = path.join(projectDir || process.cwd(), '.monomind', 'metrics');
3076
+ let tokens = {}, swarm = {}, events = [];
3077
+ try { tokens = JSON.parse(fs.readFileSync(path.join(base, 'token-summary.json'), 'utf8')); } catch(_) {}
3078
+ try { swarm = JSON.parse(fs.readFileSync(path.join(base, 'swarm-activity.json'), 'utf8')); } catch(_) {}
3079
+ try {
3080
+ const evPath = path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl');
3081
+ const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-20);
3082
+ events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
3083
+ } catch(_) {}
3084
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3085
+ res.end(JSON.stringify({ tokens, swarm, recentEvents: events }));
3086
+ } catch(_) { res.writeHead(500); res.end('{"tokens":{},"swarm":{},"recentEvents":[]}'); }
3087
+ return;
3088
+ }
3089
+
2352
3090
  // ------------------------------------------------------------------ 404
2353
3091
  res.writeHead(404, { 'Content-Type': 'text/plain' });
2354
3092
  res.end('Not found');