@monoes/monomindcli 2.16.12 → 2.16.13

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 (217) hide show
  1. package/.claude/agents/core/coder.md +1 -1
  2. package/.claude/agents/engineering/engineering-devops-automator.md +1 -1
  3. package/.claude/agents/github/repo-architect.md +1 -1
  4. package/.claude/agents/testing/production-validator.md +1 -1
  5. package/.claude/agents/testing/testing-tool-evaluator.md +1 -1
  6. package/.claude/commands/github/pr-manager.md +1 -1
  7. package/.claude/helpers/pick-rank.cjs +34 -4
  8. package/.claude/skills/monolean-audit/SKILL.md +1 -1
  9. package/dist/src/commands/cleanup-data.d.ts +30 -0
  10. package/dist/src/commands/cleanup-data.d.ts.map +1 -0
  11. package/dist/src/commands/cleanup-data.js +138 -0
  12. package/dist/src/commands/cleanup-data.js.map +1 -0
  13. package/dist/src/commands/cleanup-scratch.d.ts +43 -0
  14. package/dist/src/commands/cleanup-scratch.d.ts.map +1 -0
  15. package/dist/src/commands/cleanup-scratch.js +125 -0
  16. package/dist/src/commands/cleanup-scratch.js.map +1 -0
  17. package/dist/src/commands/cleanup.d.ts +2 -39
  18. package/dist/src/commands/cleanup.d.ts.map +1 -1
  19. package/dist/src/commands/cleanup.js +5 -247
  20. package/dist/src/commands/cleanup.js.map +1 -1
  21. package/dist/src/commands/doctor-gitignore-checks.d.ts +10 -0
  22. package/dist/src/commands/doctor-gitignore-checks.d.ts.map +1 -0
  23. package/dist/src/commands/doctor-gitignore-checks.js +141 -0
  24. package/dist/src/commands/doctor-gitignore-checks.js.map +1 -0
  25. package/dist/src/commands/doctor-helpers-checks.d.ts +10 -0
  26. package/dist/src/commands/doctor-helpers-checks.d.ts.map +1 -0
  27. package/dist/src/commands/doctor-helpers-checks.js +292 -0
  28. package/dist/src/commands/doctor-helpers-checks.js.map +1 -0
  29. package/dist/src/commands/doctor-memory-checks.d.ts +47 -0
  30. package/dist/src/commands/doctor-memory-checks.d.ts.map +1 -0
  31. package/dist/src/commands/doctor-memory-checks.js +298 -0
  32. package/dist/src/commands/doctor-memory-checks.js.map +1 -0
  33. package/dist/src/commands/doctor-monograph-checks.d.ts +9 -0
  34. package/dist/src/commands/doctor-monograph-checks.d.ts.map +1 -0
  35. package/dist/src/commands/doctor-monograph-checks.js +331 -0
  36. package/dist/src/commands/doctor-monograph-checks.js.map +1 -0
  37. package/dist/src/commands/doctor-project-checks.d.ts +7 -66
  38. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  39. package/dist/src/commands/doctor-project-checks.js +10 -1331
  40. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  41. package/dist/src/commands/doctor-routing-checks.d.ts +10 -0
  42. package/dist/src/commands/doctor-routing-checks.d.ts.map +1 -0
  43. package/dist/src/commands/doctor-routing-checks.js +194 -0
  44. package/dist/src/commands/doctor-routing-checks.js.map +1 -0
  45. package/dist/src/commands/doctor-worker-checks.d.ts +17 -0
  46. package/dist/src/commands/doctor-worker-checks.d.ts.map +1 -0
  47. package/dist/src/commands/doctor-worker-checks.js +122 -0
  48. package/dist/src/commands/doctor-worker-checks.js.map +1 -0
  49. package/dist/src/commands/hooks-routing-commands.js +1 -1
  50. package/dist/src/commands/hooks-routing-commands.js.map +1 -1
  51. package/dist/src/commands/init.d.ts.map +1 -1
  52. package/dist/src/commands/init.js +6 -15
  53. package/dist/src/commands/init.js.map +1 -1
  54. package/dist/src/index.js +2 -2
  55. package/dist/src/index.js.map +1 -1
  56. package/dist/src/init/never-commit.d.ts +3 -3
  57. package/dist/src/init/types.d.ts +1 -1
  58. package/dist/src/init/types.js +1 -1
  59. package/dist/src/knowledge/document-store.js +1 -1
  60. package/dist/src/knowledge/document-store.js.map +1 -1
  61. package/dist/src/mcp-tools/hooks-edit-command.d.ts +10 -0
  62. package/dist/src/mcp-tools/hooks-edit-command.d.ts.map +1 -0
  63. package/dist/src/mcp-tools/hooks-edit-command.js +247 -0
  64. package/dist/src/mcp-tools/hooks-edit-command.js.map +1 -0
  65. package/dist/src/mcp-tools/hooks-learning.d.ts +9 -0
  66. package/dist/src/mcp-tools/hooks-learning.d.ts.map +1 -0
  67. package/dist/src/mcp-tools/hooks-learning.js +408 -0
  68. package/dist/src/mcp-tools/hooks-learning.js.map +1 -0
  69. package/dist/src/mcp-tools/hooks-metrics-list.d.ts +8 -0
  70. package/dist/src/mcp-tools/hooks-metrics-list.d.ts.map +1 -0
  71. package/dist/src/mcp-tools/hooks-metrics-list.js +205 -0
  72. package/dist/src/mcp-tools/hooks-metrics-list.js.map +1 -0
  73. package/dist/src/mcp-tools/hooks-route.d.ts +12 -0
  74. package/dist/src/mcp-tools/hooks-route.d.ts.map +1 -0
  75. package/dist/src/mcp-tools/hooks-route.js +251 -0
  76. package/dist/src/mcp-tools/hooks-route.js.map +1 -0
  77. package/dist/src/mcp-tools/hooks-routing.d.ts +6 -27
  78. package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
  79. package/dist/src/mcp-tools/hooks-routing.js +6 -1727
  80. package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
  81. package/dist/src/mcp-tools/hooks-session.d.ts +9 -0
  82. package/dist/src/mcp-tools/hooks-session.d.ts.map +1 -0
  83. package/dist/src/mcp-tools/hooks-session.js +243 -0
  84. package/dist/src/mcp-tools/hooks-session.js.map +1 -0
  85. package/dist/src/mcp-tools/hooks-task.d.ts +17 -0
  86. package/dist/src/mcp-tools/hooks-task.d.ts.map +1 -0
  87. package/dist/src/mcp-tools/hooks-task.js +427 -0
  88. package/dist/src/mcp-tools/hooks-task.js.map +1 -0
  89. package/dist/src/mcp-tools/hooks-tools.d.ts +2 -1
  90. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  91. package/dist/src/mcp-tools/hooks-tools.js +2 -1
  92. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  93. package/dist/src/mcp-tools/memory-tools.js +1 -1
  94. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  95. package/dist/src/memory/bm25-index.d.ts +2 -2
  96. package/dist/src/memory/bm25-index.js +2 -2
  97. package/dist/src/memory/embedding-operations.js +2 -2
  98. package/dist/src/memory/embedding-operations.js.map +1 -1
  99. package/dist/src/memory/entity-name-key.d.ts +1 -1
  100. package/dist/src/memory/entity-name-key.js +1 -1
  101. package/dist/src/memory/hnsw-operations.d.ts +1 -1
  102. package/dist/src/memory/hnsw-operations.js +1 -1
  103. package/dist/src/memory/memory-bridge-backend.d.ts +43 -0
  104. package/dist/src/memory/memory-bridge-backend.d.ts.map +1 -0
  105. package/dist/src/memory/memory-bridge-backend.js +363 -0
  106. package/dist/src/memory/memory-bridge-backend.js.map +1 -0
  107. package/dist/src/memory/memory-bridge-core.d.ts +42 -0
  108. package/dist/src/memory/memory-bridge-core.d.ts.map +1 -0
  109. package/dist/src/memory/memory-bridge-core.js +126 -0
  110. package/dist/src/memory/memory-bridge-core.js.map +1 -0
  111. package/dist/src/memory/memory-bridge-entries.d.ts +131 -0
  112. package/dist/src/memory/memory-bridge-entries.d.ts.map +1 -0
  113. package/dist/src/memory/memory-bridge-entries.js +227 -0
  114. package/dist/src/memory/memory-bridge-entries.js.map +1 -0
  115. package/dist/src/memory/memory-bridge-feedback.d.ts +60 -0
  116. package/dist/src/memory/memory-bridge-feedback.d.ts.map +1 -0
  117. package/dist/src/memory/memory-bridge-feedback.js +118 -0
  118. package/dist/src/memory/memory-bridge-feedback.js.map +1 -0
  119. package/dist/src/memory/memory-bridge-paths.d.ts +123 -0
  120. package/dist/src/memory/memory-bridge-paths.d.ts.map +1 -0
  121. package/dist/src/memory/memory-bridge-paths.js +335 -0
  122. package/dist/src/memory/memory-bridge-paths.js.map +1 -0
  123. package/dist/src/memory/memory-bridge-search.d.ts +48 -0
  124. package/dist/src/memory/memory-bridge-search.d.ts.map +1 -0
  125. package/dist/src/memory/memory-bridge-search.js +383 -0
  126. package/dist/src/memory/memory-bridge-search.js.map +1 -0
  127. package/dist/src/memory/memory-bridge-sessions.d.ts +38 -0
  128. package/dist/src/memory/memory-bridge-sessions.d.ts.map +1 -0
  129. package/dist/src/memory/memory-bridge-sessions.js +109 -0
  130. package/dist/src/memory/memory-bridge-sessions.js.map +1 -0
  131. package/dist/src/memory/memory-bridge-store.d.ts +58 -0
  132. package/dist/src/memory/memory-bridge-store.d.ts.map +1 -0
  133. package/dist/src/memory/memory-bridge-store.js +201 -0
  134. package/dist/src/memory/memory-bridge-store.js.map +1 -0
  135. package/dist/src/memory/memory-bridge.d.ts +10 -438
  136. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  137. package/dist/src/memory/memory-bridge.js +14 -1787
  138. package/dist/src/memory/memory-bridge.js.map +1 -1
  139. package/dist/src/memory/memory-kg-claims.d.ts +52 -0
  140. package/dist/src/memory/memory-kg-claims.d.ts.map +1 -0
  141. package/dist/src/memory/memory-kg-claims.js +99 -0
  142. package/dist/src/memory/memory-kg-claims.js.map +1 -0
  143. package/dist/src/memory/memory-kg-index.d.ts +86 -0
  144. package/dist/src/memory/memory-kg-index.d.ts.map +1 -0
  145. package/dist/src/memory/memory-kg-index.js +256 -0
  146. package/dist/src/memory/memory-kg-index.js.map +1 -0
  147. package/dist/src/memory/memory-kg-ingest.d.ts +44 -0
  148. package/dist/src/memory/memory-kg-ingest.d.ts.map +1 -0
  149. package/dist/src/memory/memory-kg-ingest.js +361 -0
  150. package/dist/src/memory/memory-kg-ingest.js.map +1 -0
  151. package/dist/src/memory/memory-kg-model.d.ts +168 -0
  152. package/dist/src/memory/memory-kg-model.d.ts.map +1 -0
  153. package/dist/src/memory/memory-kg-model.js +158 -0
  154. package/dist/src/memory/memory-kg-model.js.map +1 -0
  155. package/dist/src/memory/memory-kg-names.d.ts +64 -0
  156. package/dist/src/memory/memory-kg-names.d.ts.map +1 -0
  157. package/dist/src/memory/memory-kg-names.js +187 -0
  158. package/dist/src/memory/memory-kg-names.js.map +1 -0
  159. package/dist/src/memory/memory-kg-promote.d.ts +79 -0
  160. package/dist/src/memory/memory-kg-promote.d.ts.map +1 -0
  161. package/dist/src/memory/memory-kg-promote.js +248 -0
  162. package/dist/src/memory/memory-kg-promote.js.map +1 -0
  163. package/dist/src/memory/memory-kg-rebuild.d.ts +77 -0
  164. package/dist/src/memory/memory-kg-rebuild.d.ts.map +1 -0
  165. package/dist/src/memory/memory-kg-rebuild.js +299 -0
  166. package/dist/src/memory/memory-kg-rebuild.js.map +1 -0
  167. package/dist/src/memory/memory-kg-rollback.d.ts +47 -0
  168. package/dist/src/memory/memory-kg-rollback.d.ts.map +1 -0
  169. package/dist/src/memory/memory-kg-rollback.js +193 -0
  170. package/dist/src/memory/memory-kg-rollback.js.map +1 -0
  171. package/dist/src/memory/memory-kg-rules.d.ts +64 -0
  172. package/dist/src/memory/memory-kg-rules.d.ts.map +1 -0
  173. package/dist/src/memory/memory-kg-rules.js +251 -0
  174. package/dist/src/memory/memory-kg-rules.js.map +1 -0
  175. package/dist/src/memory/memory-kg-scan.d.ts +118 -0
  176. package/dist/src/memory/memory-kg-scan.d.ts.map +1 -0
  177. package/dist/src/memory/memory-kg-scan.js +209 -0
  178. package/dist/src/memory/memory-kg-scan.js.map +1 -0
  179. package/dist/src/memory/memory-kg-search.d.ts +80 -0
  180. package/dist/src/memory/memory-kg-search.d.ts.map +1 -0
  181. package/dist/src/memory/memory-kg-search.js +239 -0
  182. package/dist/src/memory/memory-kg-search.js.map +1 -0
  183. package/dist/src/memory/memory-kg.d.ts +19 -547
  184. package/dist/src/memory/memory-kg.d.ts.map +1 -1
  185. package/dist/src/memory/memory-kg.js +10 -2389
  186. package/dist/src/memory/memory-kg.js.map +1 -1
  187. package/dist/src/ui/org-hil.mjs +1 -1
  188. package/dist/src/ui/routes-org-agents.mjs +318 -0
  189. package/dist/src/ui/routes-org-approvals.mjs +133 -0
  190. package/dist/src/ui/routes-org-config.mjs +459 -0
  191. package/dist/src/ui/routes-org-control.mjs +300 -0
  192. package/dist/src/ui/routes-org-files.mjs +172 -0
  193. package/dist/src/ui/routes-org-helpers.mjs +84 -0
  194. package/dist/src/ui/routes-org-knowledge.mjs +262 -0
  195. package/dist/src/ui/routes-org-lifecycle.mjs +224 -0
  196. package/dist/src/ui/routes-org-live.mjs +278 -0
  197. package/dist/src/ui/routes-org-mastermind.mjs +346 -0
  198. package/dist/src/ui/routes-org-planning.mjs +398 -0
  199. package/dist/src/ui/routes-org-runs.mjs +430 -0
  200. package/dist/src/ui/routes-org-status.mjs +342 -0
  201. package/dist/src/ui/routes-org-workflows.mjs +212 -0
  202. package/dist/src/ui/routes-org.mjs +26 -3847
  203. package/dist/src/ui/server.mjs +1 -1
  204. package/dist/src/utils/native-error.d.ts +1 -1
  205. package/dist/src/utils/native-error.js +1 -1
  206. package/dist/src/utils/paths.d.ts +2 -1
  207. package/dist/src/utils/paths.d.ts.map +1 -1
  208. package/dist/src/utils/paths.js +2 -1
  209. package/dist/src/utils/paths.js.map +1 -1
  210. package/dist/tsconfig.tsbuildinfo +1 -1
  211. package/org-skills/adversarial-reviewer/SKILL.md +1 -1
  212. package/org-skills/board-deck-builder/SKILL.md +1 -1
  213. package/org-skills/code-reviewer/SKILL.md +1 -1
  214. package/org-skills/debugging-wizard/SKILL.md +1 -1
  215. package/org-skills/scientific-thinking-literature-review/SKILL.md +1 -1
  216. package/org-skills/scientific-thinking-scholar-evaluation/SKILL.md +1 -1
  217. package/package.json +8 -8
@@ -1,3851 +1,30 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- // forwarder.js is compiled from orgrt/forwarder.ts (dist/src/orgrt/forwarder.js
5
- // sits alongside this file's own compiled dist/src/ui/routes-org.mjs after
6
- // build; under vitest the .js specifier resolves straight to the .ts source,
7
- // same as every other cross-reference in this codebase). translate()/
8
- // companionEvents() are pure — no filesystem or process side effects — so
9
- // importing them here doesn't pull in attachForwarder's spawn/heal logic.
10
- import { companionEvents, translate } from '../orgrt/forwarder.js';
11
- import * as hil from './org-hil.mjs';
12
- import * as orgRuntime from './org-runtime.mjs';
13
- import { getMmClientCount } from './sse-manager.mjs';
14
-
15
- /** listApprovals for read-only views, where an unreadable file shows as none. */
16
- function approvalsOrEmpty(orgsDir, orgName) {
17
- try {
18
- return hil.listApprovals(path.dirname(path.dirname(orgsDir)), orgName);
19
- } catch (_) {
20
- return [];
21
- }
22
- }
23
-
24
- const validOrgName = (n) => typeof n === 'string' && /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(n);
25
-
26
- /** Project root for a HIL route: ?dir= or the server's project. */
27
- function hilRoot(req, ctx) {
28
- const dir = new URL(req.url, 'http://localhost').searchParams.get('dir');
29
- return path.resolve(dir || ctx.projectDir || process.cwd());
30
- }
31
-
32
- const HIL_BODY_MAX = 65536;
33
-
34
- /** JSON body (≤64KB); null when malformed. Call inside hilRespond: an
35
- * oversized body throws 413 and an aborted upload 400, instead of an
36
- * unhandled rejection that would take the dashboard process down. Bytes are
37
- * decoded once at the end, so a character split across chunks survives. */
38
- async function readHilBody(req) {
39
- const chunks = [];
40
- let size = 0;
41
- try {
42
- // Past the cap, keep draining (without keeping) so the 413 still reaches
43
- // the client — leaving the iterator early destroys the socket.
44
- for await (const chunk of req) {
45
- size += chunk.length;
46
- if (size <= HIL_BODY_MAX) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
47
- }
48
- } catch (err) {
49
- throw Object.assign(new Error(`request body not received: ${err.message}`), { status: 400 });
50
- }
51
- if (size > HIL_BODY_MAX)
52
- throw Object.assign(new Error('request body is over 64KB'), { status: 413 });
53
- try {
54
- return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
55
- } catch (_) {
56
- return null;
57
- }
58
- }
59
-
60
- /** decodeURIComponent for a path segment; a malformed escape is a 400. */
61
- function decodeHilSegment(seg) {
62
- try {
63
- return decodeURIComponent(seg);
64
- } catch (_) {
65
- throw Object.assign(new Error(`malformed path segment: ${seg}`), { status: 400 });
66
- }
67
- }
68
-
69
- /** Run `fn` and answer with its JSON result, or the HIL error's status. */
70
- async function hilRespond(res, corsOrigin, fn) {
71
- const cors = corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {};
72
- try {
73
- const data = await fn();
74
- res.writeHead(200, { 'Content-Type': 'application/json', ...cors });
75
- res.end(JSON.stringify(data));
76
- } catch (err) {
77
- const { status, error } = err.status
78
- ? { status: err.status, error: err.message }
79
- : hil.hilErrorStatus(err);
80
- res.writeHead(status, { 'Content-Type': 'application/json', ...cors });
81
- res.end(JSON.stringify({ ok: false, error }));
82
- }
83
- return true;
84
- }
85
-
86
- /** Record a human decision in the dashboard's event log and push it to live views. */
87
- function hilEvent(ctx, root, ev) {
88
- const event = { ...ev, resolvedBy: hil.DASHBOARD_RESOLVER, ts: Date.now() };
89
- ctx
90
- .appendToFile(path.join(root, 'data', 'mastermind-events.jsonl'), `${JSON.stringify(event)}\n`)
91
- .catch(() => {});
92
- ctx.broadcastMm(event);
93
- }
1
+ import { handleOrgAgentRoutes } from './routes-org-agents.mjs';
2
+ import { handleOrgApprovalRoutes } from './routes-org-approvals.mjs';
3
+ import { handleOrgConfigRoutes } from './routes-org-config.mjs';
4
+ import { handleOrgControlRoutes } from './routes-org-control.mjs';
5
+ import { handleOrgFileRoutes } from './routes-org-files.mjs';
6
+ import { handleOrgKnowledgeRoutes } from './routes-org-knowledge.mjs';
7
+ import { handleOrgLifecycleRoutes } from './routes-org-lifecycle.mjs';
8
+ import { handleOrgLiveRoutes } from './routes-org-live.mjs';
9
+ import { handleOrgMastermindRoutes } from './routes-org-mastermind.mjs';
10
+ import { handleOrgPlanningRoutes } from './routes-org-planning.mjs';
11
+ import { handleOrgRunRoutes } from './routes-org-runs.mjs';
12
+ import { handleOrgStatusRoutes } from './routes-org-status.mjs';
13
+ import { handleOrgWorkflowRoutes } from './routes-org-workflows.mjs';
94
14
 
95
15
  export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
96
- // ------------------------------------------------- Org management
97
- // GET /api/orgs — list all saved org configs
98
- if (req.method === 'GET' && url === '/api/orgs') {
99
- try {
100
- const _orgsQs = new URL(req.url, 'http://localhost').searchParams;
101
- const _orgsExplicitDir = _orgsQs.get('dir');
102
- const _orgsServerRoot = path.resolve(_orgsExplicitDir || ctx.projectDir || process.cwd());
103
- // Collect project dirs to search: explicit dir + known-projects (like sessions API)
104
- const _orgsProjDirs = new Set([_orgsServerRoot]);
105
- if (!_orgsExplicitDir) {
106
- try {
107
- const _knownOrgsFile = path.join(_orgsServerRoot, 'data', 'known-projects.json');
108
- if (fs.existsSync(_knownOrgsFile)) {
109
- JSON.parse(fs.readFileSync(_knownOrgsFile, 'utf8')).forEach((p) =>
110
- _orgsProjDirs.add(p),
111
- );
112
- }
113
- } catch (_) {}
114
- }
115
- const _sidecarSuffixRe =
116
- /-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets|join-requests|skills)\.json$/;
117
- const _orgsSeen = new Set();
118
- const orgs = [];
119
- for (const _opd of _orgsProjDirs) {
120
- const orgsDir = path.join(_opd, '.monomind', 'orgs');
121
- if (!fs.existsSync(orgsDir)) continue;
122
- const files = fs
123
- .readdirSync(orgsDir)
124
- .filter((f) => f.endsWith('.json') && !_sidecarSuffixRe.test(f));
125
- for (const f of files) {
126
- try {
127
- const cfg = JSON.parse(fs.readFileSync(path.join(orgsDir, f), 'utf8'));
128
- const _lOrgName = cfg.name || '';
129
- if (!_lOrgName || _orgsSeen.has(_lOrgName)) continue;
130
- _orgsSeen.add(_lOrgName);
131
- const _rs = ctx._readRunState(_lOrgName, _opd);
132
- const _ttl = Math.max((_rs?.checkpointInterval || 600000) * 2, 7200000);
133
- const running =
134
- (_rs?.status === 'running' && Date.now() - (_rs?.lastEventAt || 0) < _ttl) ||
135
- ctx.activeOrgRuns.has(_lOrgName);
136
- orgs.push({
137
- name: cfg.name,
138
- goal: cfg.goal,
139
- roles: Array.isArray(cfg.roles) ? cfg.roles : [],
140
- topology: cfg.topology,
141
- created_at: cfg.created_at,
142
- running,
143
- status: cfg.status,
144
- projectDir: _opd,
145
- lastEventAt: _rs?.lastEventAt || null,
146
- loop: cfg.loop
147
- ? {
148
- poll_interval_minutes: cfg.loop.poll_interval_minutes,
149
- last_run: cfg.loop.last_run,
150
- next_run: cfg.loop.next_run,
151
- }
152
- : undefined,
153
- });
154
- } catch (_) {}
155
- }
156
- }
157
- res.writeHead(200, {
158
- 'Content-Type': 'application/json',
159
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
160
- });
161
- res.end(JSON.stringify(orgs));
162
- } catch (_) {
163
- res.writeHead(500);
164
- res.end('[]');
165
- }
166
- return true;
167
- }
168
-
169
- // POST /api/orgs/:name/import — import an org config by name (orgs.html upload flow)
170
- if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/import$/i.test(url)) {
171
- let body = '';
172
- req.on('data', (c) => {
173
- body += c;
174
- if (body.length > 2e6) req.destroy();
175
- });
176
- req.on('end', () => {
177
- try {
178
- const urlParts = url.split('/');
179
- const orgName = decodeURIComponent(urlParts[3]);
180
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
181
- res.writeHead(400, { 'Content-Type': 'application/json' });
182
- res.end(JSON.stringify({ error: 'Invalid org name' }));
183
- return;
184
- }
185
- const cfg = JSON.parse(body);
186
- const _importQs = new URL(req.url, 'http://localhost').searchParams;
187
- const dir = path.resolve(_importQs.get('dir') || ctx.projectDir || process.cwd());
188
- const orgsDir = path.join(dir, '.monomind', 'orgs');
189
- fs.mkdirSync(orgsDir, { recursive: true });
190
- const destFile = path.join(orgsDir, `${orgName}.json`);
191
- fs.writeFileSync(destFile, JSON.stringify({ ...cfg, name: orgName }, null, 2), 'utf8');
192
- res.writeHead(200, {
193
- 'Content-Type': 'application/json',
194
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
195
- });
196
- res.end(JSON.stringify({ ok: true, name: orgName, file: destFile }));
197
- } catch (e) {
198
- res.writeHead(400, { 'Content-Type': 'application/json' });
199
- res.end(JSON.stringify({ error: e.message }));
200
- }
201
- });
202
- return true;
203
- }
204
-
205
- // POST /api/orgs — import / create org from JSON body
206
- if (req.method === 'POST' && url === '/api/orgs') {
207
- let body = '';
208
- req.on('data', (c) => {
209
- body += c;
210
- if (body.length > 2e6) req.destroy();
211
- });
212
- req.on('end', () => {
213
- try {
214
- const cfg = JSON.parse(body);
215
- const qs = new URL(req.url, 'http://localhost').searchParams;
216
- const dir = qs.get('dir') || cfg.dir || ctx.projectDir || process.cwd();
217
- const name = (cfg.name || '')
218
- .toLowerCase()
219
- .replace(/[^a-z0-9_-]/g, '-')
220
- .replace(/^-+|-+$/g, '')
221
- .slice(0, 64);
222
- if (!name) {
223
- res.writeHead(400, { 'Content-Type': 'application/json' });
224
- res.end(JSON.stringify({ error: 'Invalid org name' }));
225
- return;
226
- }
227
- const orgsDir = path.join(path.resolve(dir), '.monomind', 'orgs');
228
- fs.mkdirSync(orgsDir, { recursive: true });
229
- const destFile = path.join(orgsDir, `${name}.json`);
230
- const cleanCfg = Object.fromEntries(
231
- Object.entries({ ...cfg, name }).filter(([k]) => !k.startsWith('_')),
232
- );
233
- fs.writeFileSync(destFile, JSON.stringify(cleanCfg, null, 2), 'utf8');
234
- res.writeHead(200, {
235
- 'Content-Type': 'application/json',
236
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
237
- });
238
- res.end(JSON.stringify({ ok: true, name, file: destFile }));
239
- } catch (e) {
240
- res.writeHead(400, { 'Content-Type': 'application/json' });
241
- res.end(JSON.stringify({ error: e.message }));
242
- }
243
- });
244
- return true;
245
- }
246
-
247
- // GET /api/orgs/:name — get specific org config (exact path: /api/orgs/<slug>)
248
- if (req.method === 'GET' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
249
- try {
250
- const orgName = decodeURIComponent(url.slice('/api/orgs/'.length));
251
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
252
- res.writeHead(400);
253
- res.end('Invalid org name');
254
- return true;
255
- }
256
- const _orgsOneQs = new URL(req.url, 'http://localhost').searchParams;
257
- const _orgsOneRoot = path.resolve(_orgsOneQs.get('dir') || ctx.projectDir || process.cwd());
258
- const _orgsOneProjDir = ctx._resolveOrgProjectDir(orgName, _orgsOneRoot) || _orgsOneRoot;
259
- const f = path.join(_orgsOneProjDir, '.monomind', 'orgs', `${orgName}.json`);
260
- if (!fs.existsSync(f)) {
261
- res.writeHead(404);
262
- res.end('{"error":"not found"}');
263
- return true;
264
- }
265
- res.writeHead(200, {
266
- 'Content-Type': 'application/json',
267
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
268
- });
269
- res.end(fs.readFileSync(f, 'utf8'));
270
- } catch (_) {
271
- res.writeHead(500);
272
- res.end('{}');
273
- }
274
- return true;
275
- }
276
-
277
- // GET /api/org/:name — ORG ROOM: rich org data (config + state + tasks + routines + goals)
278
- if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
279
- try {
280
- const orgName = decodeURIComponent(url.slice('/api/org/'.length));
281
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
282
- res.writeHead(400);
283
- res.end('Invalid org name');
284
- return true;
285
- }
286
- const _orgQs = new URL(req.url, 'http://localhost').searchParams;
287
- const _orgServerRoot = path.resolve(_orgQs.get('dir') || ctx.projectDir || process.cwd());
288
- // Resolve which project dir actually has this org's config
289
- const d = ctx._resolveOrgProjectDir(orgName, _orgServerRoot) || _orgServerRoot;
290
- const orgsDir = path.join(d, '.monomind', 'orgs');
291
-
292
- const readJsonSafe = (f) => {
293
- try {
294
- return JSON.parse(fs.readFileSync(f, 'utf8'));
295
- } catch (_) {
296
- return null;
297
- }
298
- };
299
-
300
- const configFile = path.join(orgsDir, `${orgName}.json`);
301
- if (!fs.existsSync(configFile)) {
302
- res.writeHead(404);
303
- res.end('{"error":"org not found"}');
304
- return true;
305
- }
306
- const config = readJsonSafe(configFile);
307
-
308
- const state = readJsonSafe(path.join(orgsDir, `${orgName}-state.json`)) || { agents: {} };
309
- const goalsData = readJsonSafe(path.join(orgsDir, `${orgName}-goals.json`)) || { goals: [] };
310
- const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || {
311
- routines: [],
312
- };
313
- const approvalsData = { approvals: approvalsOrEmpty(orgsDir, orgName) };
314
-
315
- // Check running status: stop file absence AND (in-memory ctx.activeOrgRuns OR state-file agents OR active loop file)
316
- // Path must match what `org run`'s poll loop and `org serve`'s pollStopfiles()
317
- // actually watch (.monomind/orgs/<name>/stop) — see the POST .../stop handler below.
318
- const stopFile = path.join(orgsDir, orgName, 'stop');
319
- const _loopsDir = path.join(d, '.monomind', 'loops');
320
- const _loopRunning = (() => {
321
- try {
322
- if (!fs.existsSync(_loopsDir)) return false;
323
- // Get the org's state file mtime to correlate with loop activity
324
- const orgStateMtime = (() => {
325
- try {
326
- return fs.statSync(path.join(orgsDir, `${orgName}-state.json`)).mtimeMs;
327
- } catch {
328
- return 0;
329
- }
330
- })();
331
- // Also check org's most recent run file mtime
332
- const orgRunsDir = path.join(
333
- ctx._getGitMonomindDir(d) || path.join(d, '.monomind'),
334
- 'orgs',
335
- orgName,
336
- 'runs',
337
- );
338
- const orgLastRunMtime = (() => {
339
- try {
340
- if (!fs.existsSync(orgRunsDir)) return 0;
341
- const runFiles = fs
342
- .readdirSync(orgRunsDir)
343
- .filter((f) => f.endsWith('.jsonl') && !f.startsWith('._'));
344
- if (!runFiles.length) return 0;
345
- return Math.max(
346
- ...runFiles.map((f) => {
347
- try {
348
- return fs.statSync(path.join(orgRunsDir, f)).mtimeMs;
349
- } catch {
350
- return 0;
351
- }
352
- }),
353
- );
354
- } catch {
355
- return 0;
356
- }
357
- })();
358
- const orgLastActivity = Math.max(orgStateMtime, orgLastRunMtime);
359
- return fs.readdirSync(_loopsDir).some((f) => {
360
- if (!f.endsWith('.json') || f.endsWith('.stop')) return false;
361
- try {
362
- const lp = JSON.parse(fs.readFileSync(path.join(_loopsDir, f), 'utf8'));
363
- if (!lp.command?.includes('runorg')) return false;
364
- if (!['running', 'paused'].includes(lp.status)) return false;
365
- // Primary match: explicit orgName field (written by runorg command since v1.14.2)
366
- if (lp.orgName === orgName) return true;
367
- // Fallback: org name in prompt (early loop files that preserved --org flag)
368
- if ((lp.prompt || '').includes(orgName)) return true;
369
- // Heuristic: if loop's lastRunAt is within 3x wait interval of org's last activity
370
- const waitMs = (lp.wait || 60) * 3 * 1000;
371
- return (
372
- orgLastActivity > 0 && Math.abs(orgLastActivity - (lp.lastRunAt || 0)) < waitMs
373
- );
374
- } catch {
375
- return false;
376
- }
377
- });
378
- } catch {
379
- return false;
380
- }
381
- })();
382
- const _runstateData = ctx._readRunState(orgName, d);
383
- const _runstateTtl = Math.max((_runstateData?.checkpointInterval || 600000) * 2, 7200000);
384
- const _runstateAlive =
385
- _runstateData?.status === 'running' &&
386
- Date.now() - (_runstateData?.lastEventAt || 0) < _runstateTtl;
387
- const running =
388
- !fs.existsSync(stopFile) &&
389
- (_runstateAlive || ctx.activeOrgRuns.has(orgName) || _loopRunning);
390
-
391
- // Read real tasks from the task store and group by status column
392
- const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
393
- const allTasks = taskStoreData ? Object.values(taskStoreData.tasks || {}) : [];
394
- const tasks = {
395
- todo: allTasks
396
- .filter((t) => t.status === 'pending')
397
- .map((t) => ({
398
- id: t.taskId,
399
- description: t.description,
400
- status: 'todo',
401
- ts: t.createdAt,
402
- })),
403
- doing: allTasks
404
- .filter((t) => t.status === 'in_progress')
405
- .map((t) => ({
406
- id: t.taskId,
407
- description: t.description,
408
- status: 'doing',
409
- ts: t.startedAt || t.createdAt,
410
- })),
411
- done: allTasks
412
- .filter(
413
- (t) => t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled',
414
- )
415
- .map((t) => ({
416
- id: t.taskId,
417
- description: t.description,
418
- status: t.status,
419
- ts: t.completedAt || t.createdAt,
420
- })),
421
- };
422
-
423
- const result = {
424
- config,
425
- state,
426
- goals: goalsData.goals,
427
- routines: routinesData.routines,
428
- approvals: approvalsData.approvals,
429
- running,
430
- tasks,
431
- runId: _runstateData?.runId || null,
432
- lastEventAt: _runstateData?.lastEventAt || null,
433
- agentStates: _runstateData?.agentStates || {},
434
- };
435
-
436
- res.writeHead(200, {
437
- 'Content-Type': 'application/json',
438
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
439
- });
440
- res.end(JSON.stringify(result));
441
- } catch (_) {
442
- res.writeHead(500);
443
- res.end('{}');
444
- }
445
- return true;
446
- }
447
-
448
- // GET /api/org/:name/activity — recent org events from mastermind-events.jsonl
449
- if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/activity$/i.test(url)) {
450
- try {
451
- const parts = url.split('/');
452
- const orgName = decodeURIComponent(parts[3]);
453
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
454
- res.writeHead(400);
455
- res.end('[]');
456
- return true;
457
- }
458
- const _actQs = new URL(req.url, 'http://localhost').searchParams;
459
- const _actServerRoot = path.resolve(_actQs.get('dir') || ctx.projectDir || process.cwd());
460
- const d = ctx._resolveOrgProjectDir(orgName, _actServerRoot) || _actServerRoot;
461
- const orgsDir = path.join(d, '.monomind', 'orgs');
462
- const readJ = (f) => {
463
- try {
464
- return JSON.parse(fs.readFileSync(f, 'utf8'));
465
- } catch (_) {
466
- return null;
467
- }
468
- };
469
- const events = [];
470
-
471
- // 1) Global mastermind events that EXPLICITLY belong to this org (strict — no untagged leak)
472
- const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
473
- if (fs.existsSync(eventsFile)) {
474
- const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean);
475
- for (const l of lines.slice(-1000)) {
476
- try {
477
- const e = JSON.parse(l);
478
- if (e && e.org === orgName) events.push(e);
479
- } catch (_) {}
480
- }
481
- }
482
-
483
- // 2) Synthesize an org-scoped timeline from this org's own records (real data, distinct per org)
484
- const cfg = readJ(path.join(orgsDir, `${orgName}.json`));
485
- if (cfg) {
486
- const createdMs = cfg.created_at ? Date.parse(cfg.created_at) : null;
487
- if (createdMs)
488
- events.push({
489
- type: 'org:create',
490
- ts: createdMs,
491
- msg: String(cfg.goal || 'Org created').slice(0, 80),
492
- });
493
- // Roles are defined atomically at org creation — there is no per-role
494
- // timestamp in the config, so every role:defined event uses the org's
495
- // real created_at instead of a fabricated per-index offset.
496
- (cfg.roles || []).forEach((r) => {
497
- events.push({
498
- type: 'role:defined',
499
- ts: createdMs,
500
- role: r.title || r.id,
501
- msg: r.agent_type || '',
502
- });
503
- });
504
- }
505
- const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
506
- (goals?.goals || []).forEach((g) =>
507
- events.push({
508
- type: 'goal',
509
- ts: Date.parse(g.created_at || g.updated_at || '') || null,
510
- role: g.status || '',
511
- msg: String(g.text || g.title || g.goal || '').slice(0, 80),
512
- }),
513
- );
514
- approvalsOrEmpty(orgsDir, orgName).forEach((a) => {
515
- events.push({
516
- type: 'approval',
517
- ts: a.ts,
518
- role: a.roleId || '',
519
- msg: `${a.action} — ${a.status}`.slice(0, 80),
520
- });
521
- });
522
- const state = readJ(path.join(orgsDir, `${orgName}-state.json`));
523
- if (state?.agents) {
524
- for (const [aid, a] of Object.entries(state.agents)) {
525
- const raw = a.lastHeartbeat || a.last_seen || a.updated_at || null;
526
- const ts = typeof raw === 'number' ? raw : raw ? Date.parse(raw) : null;
527
- events.push({ type: 'org:heartbeat', ts, agent: aid, msg: a.status || '' });
528
- }
529
- }
530
-
531
- const out = events
532
- .filter((e) => e?.ts)
533
- .sort((a, b) => b.ts - a.ts)
534
- .slice(0, 100);
535
- res.writeHead(200, {
536
- 'Content-Type': 'application/json',
537
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
538
- });
539
- res.end(JSON.stringify(out));
540
- } catch (_) {
541
- res.writeHead(500);
542
- res.end('[]');
543
- }
544
- return true;
545
- }
546
-
547
- // GET /api/org/:name/adapters — org adapter registry
548
- if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/adapters$/i.test(url)) {
549
- try {
550
- const parts = url.split('/');
551
- const orgName = decodeURIComponent(parts[3]);
552
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
553
- res.writeHead(400);
554
- res.end('{}');
555
- return true;
556
- }
557
- const _adaptersQs = new URL(req.url, 'http://localhost').searchParams;
558
- const _adaptersRoot = path.resolve(_adaptersQs.get('dir') || ctx.projectDir || process.cwd());
559
- const d = ctx._resolveOrgProjectDir(orgName, _adaptersRoot) || _adaptersRoot;
560
- const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
561
- if (!fs.existsSync(adaptersFile)) {
562
- // Return defaults derived from org config if available
563
- const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
564
- // claude-sonnet-5 = DEFAULT_CLAUDE_MODEL (src/orgrt/vercel-providers.ts); .mjs can't import TS.
565
- let defaultAdapter = 'claude-sonnet-5';
566
- try {
567
- defaultAdapter =
568
- JSON.parse(fs.readFileSync(orgFile, 'utf8'))?.run_config?.ceo_adapter || defaultAdapter;
569
- } catch (_) {}
570
- res.writeHead(200, {
571
- 'Content-Type': 'application/json',
572
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
573
- });
574
- // This list mirrors the runtimes orgrt/daemon.ts actually registers
575
- // (resolveRunner()'s if-chain + the ClaudeAgentRunner default it falls
576
- // through to) rather than an independent, hand-maintained catalog — a
577
- // standalone 'gemini-local' entry used to be advertised here even
578
- // though no runner is ever registered for it (autoRuntimeFromProvider
579
- // has no 'gemini' case, so a role requesting it silently falls back to
580
- // Claude — see daemon.ts#startOrg's fail-fast warning). Gemini is only
581
- // actually reachable via the 'vercel' runtime (vendor: 'google') or via
582
- // 'antigravity' (Google-account CLI), both listed below. 'http' maps to
583
- // provider.kind === 'base-url' (ANTHROPIC_BASE_URL + optional auth
584
- // token) — a real, usable path, not disabled.
585
- res.end(
586
- JSON.stringify({
587
- default_adapter: defaultAdapter,
588
- adapters: [
589
- {
590
- type: 'claude-local',
591
- label: 'Claude (local CLI)',
592
- source: 'built-in',
593
- disabled: false,
594
- modelsCount: 3,
595
- },
596
- {
597
- type: 'codex-local',
598
- label: 'Codex CLI',
599
- source: 'built-in',
600
- disabled: false,
601
- modelsCount: 1,
602
- },
603
- {
604
- type: 'antigravity',
605
- label: 'Antigravity (Google CLI)',
606
- source: 'built-in',
607
- disabled: false,
608
- modelsCount: 1,
609
- },
610
- {
611
- type: 'grok',
612
- label: 'Grok CLI',
613
- source: 'built-in',
614
- disabled: false,
615
- modelsCount: 1,
616
- },
617
- {
618
- type: 'qwen',
619
- label: 'Qwen CLI',
620
- source: 'built-in',
621
- disabled: false,
622
- modelsCount: 1,
623
- },
624
- {
625
- type: 'crush',
626
- label: 'Crush CLI',
627
- source: 'built-in',
628
- disabled: false,
629
- modelsCount: 1,
630
- },
631
- {
632
- type: 'copilot',
633
- label: 'GitHub Copilot CLI',
634
- source: 'built-in',
635
- disabled: false,
636
- modelsCount: 1,
637
- },
638
- { type: 'pi', label: 'Pi CLI', source: 'built-in', disabled: false, modelsCount: 1 },
639
- {
640
- type: 'opencode',
641
- label: 'OpenCode CLI',
642
- source: 'built-in',
643
- disabled: false,
644
- modelsCount: 1,
645
- },
646
- {
647
- type: 'kimicode',
648
- label: 'KimiCode CLI',
649
- source: 'built-in',
650
- disabled: false,
651
- modelsCount: 1,
652
- },
653
- {
654
- type: 'vercel',
655
- label: 'Vercel AI SDK (multi-vendor, incl. Gemini/OpenAI)',
656
- source: 'built-in',
657
- disabled: false,
658
- modelsCount: 14,
659
- },
660
- {
661
- type: 'http',
662
- label: 'Custom HTTP (base-url provider)',
663
- source: 'built-in',
664
- disabled: false,
665
- modelsCount: 0,
666
- },
667
- ],
668
- }),
669
- );
670
- return true;
671
- }
672
- const data = JSON.parse(fs.readFileSync(adaptersFile, 'utf8'));
673
- res.writeHead(200, {
674
- 'Content-Type': 'application/json',
675
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
676
- });
677
- res.end(JSON.stringify(data));
678
- } catch (_) {
679
- res.writeHead(500);
680
- res.end('{}');
681
- }
682
- return true;
683
- }
684
-
685
- // GET /api/org/:name/agent/:roleId — full agent detail: org role + .claude/agents definition
686
- // (characteristics, responsibilities, instructions document)
687
- if (
688
- req.method === 'GET' &&
689
- /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agent\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)
690
- ) {
691
- try {
692
- const parts = url.split('/');
693
- const orgName = decodeURIComponent(parts[3]);
694
- const roleId = decodeURIComponent(parts[5]);
695
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
696
- res.writeHead(400);
697
- res.end('{}');
698
- return true;
699
- }
700
- if (roleId.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(roleId)) {
701
- res.writeHead(400);
702
- res.end('{}');
703
- return true;
704
- }
705
- const _agentQs = new URL(req.url, 'http://localhost').searchParams;
706
- const d = path.resolve(_agentQs.get('dir') || ctx.projectDir || process.cwd());
707
- const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
708
- if (!fs.existsSync(orgFile)) {
709
- res.writeHead(404);
710
- res.end('{}');
711
- return true;
712
- }
713
- const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
714
- const role = (config.roles || []).find((r) => r.id === roleId);
715
- if (!role) {
716
- res.writeHead(404);
717
- res.end('{}');
718
- return true;
719
- }
720
-
721
- const agentType = String(role.agent_type || role.type || '').toLowerCase();
722
- const wanted = [agentType, String(role.id).toLowerCase()].filter(Boolean);
723
-
724
- // Find a matching agent definition under .claude/agents (recursive); match frontmatter name then filename.
725
- const agentsDir = path.join(d, '.claude', 'agents');
726
- let definition = { found: false };
727
- if (wanted.length && fs.existsSync(agentsDir)) {
728
- const stack = [agentsDir];
729
- let nameMatch = null,
730
- slugMatch = null;
731
- while (stack.length && !nameMatch) {
732
- const dir = stack.pop();
733
- let entries = [];
734
- try {
735
- entries = fs.readdirSync(dir, { withFileTypes: true });
736
- } catch (_) {
737
- continue;
738
- }
739
- for (const e of entries) {
740
- const full = path.join(dir, e.name);
741
- if (e.isDirectory()) {
742
- stack.push(full);
743
- continue;
744
- }
745
- if (!e.name.endsWith('.md') || e.name.startsWith('_')) continue;
746
- const slug = e.name.replace(/\.md$/, '').toLowerCase();
747
- let raw = '';
748
- try {
749
- raw = fs.readFileSync(full, 'utf8');
750
- } catch (_) {
751
- continue;
752
- }
753
- const fmName = ((raw.match(/^name:\s*(.+)$/m) || [])[1] || '').trim().toLowerCase();
754
- if (fmName && wanted.includes(fmName)) {
755
- nameMatch = { full, raw };
756
- break;
757
- }
758
- if (!slugMatch && wanted.includes(slug)) slugMatch = { full, raw };
759
- }
760
- }
761
- const match = nameMatch || slugMatch;
762
- if (match) {
763
- definition = ctx.parseAgentDef(match.raw);
764
- definition.found = true;
765
- definition.file = path.relative(d, match.full);
766
- }
767
- }
768
-
769
- res.writeHead(200, {
770
- 'Content-Type': 'application/json',
771
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
772
- 'Cache-Control': 'no-cache',
773
- });
774
- res.end(JSON.stringify({ role, definition }));
775
- } catch (_) {
776
- res.writeHead(500);
777
- res.end('{}');
778
- }
779
- return true;
780
- }
781
-
782
- // POST /api/org/:name/agent/:roleId/avatar — set (or clear) a custom avatar for one
783
- // role, stored inline as a data URL on the role itself so it travels with the org
784
- // config export/import. Body: { avatarDataUrl } — null/omitted avatarDataUrl clears
785
- // the custom avatar and reverts to the built-in library picture. `dir` (like every
786
- // other dir-accepting route in this file, POST included — see /goals above) comes
787
- // from the query string, not the body.
788
- if (
789
- req.method === 'POST' &&
790
- /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agent\/[a-z0-9][a-z0-9_-]{0,63}\/avatar$/i.test(url)
791
- ) {
792
- let body = '';
793
- for await (const chunk of req) {
794
- body += chunk;
795
- if (body.length > 8388608) {
796
- req.destroy();
797
- break;
798
- }
799
- }
800
- try {
801
- const parts = url.split('/');
802
- const orgName = decodeURIComponent(parts[3]);
803
- const roleId = decodeURIComponent(parts[5]);
804
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
805
- res.writeHead(400);
806
- res.end('{"ok":false,"error":"Invalid org name"}');
807
- return true;
808
- }
809
- if (roleId.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(roleId)) {
810
- res.writeHead(400);
811
- res.end('{"ok":false,"error":"Invalid role id"}');
812
- return true;
813
- }
814
- const parsed = JSON.parse(body);
815
- const avatarDataUrl = parsed.avatarDataUrl;
816
- if (
817
- avatarDataUrl != null &&
818
- (typeof avatarDataUrl !== 'string' ||
819
- avatarDataUrl.length > 2_000_000 ||
820
- !/^data:image\/(png|jpe?g|webp|gif);base64,[A-Za-z0-9+/=]+$/.test(avatarDataUrl))
821
- ) {
822
- res.writeHead(400);
823
- res.end('{"ok":false,"error":"Invalid or oversized avatarDataUrl"}');
824
- return true;
825
- }
826
- const _avatarQs = new URL(req.url, 'http://localhost').searchParams;
827
- const d = path.resolve(_avatarQs.get('dir') || ctx.projectDir || process.cwd());
828
- const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
829
- if (!fs.existsSync(orgFile)) {
830
- res.writeHead(404);
831
- res.end('{"ok":false,"error":"org not found"}');
832
- return true;
833
- }
834
- const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
835
- const role = (config.roles || []).find((r) => r.id === roleId);
836
- if (!role) {
837
- res.writeHead(404);
838
- res.end('{"ok":false,"error":"role not found"}');
839
- return true;
840
- }
841
- if (avatarDataUrl) role.avatar = avatarDataUrl;
842
- else delete role.avatar;
843
- const tmp = `${orgFile}.tmp`;
844
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2), 'utf-8');
845
- fs.renameSync(tmp, orgFile);
846
- res.writeHead(200, {
847
- 'Content-Type': 'application/json',
848
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
849
- });
850
- res.end(JSON.stringify({ ok: true }));
851
- } catch (e) {
852
- res.writeHead(500);
853
- res.end(JSON.stringify({ ok: false, error: String(e.message || e) }));
854
- }
855
- return true;
856
- }
857
-
858
- // GET /api/org/:name/search?q=<query> — fuzzy search across org data
859
- if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/search(\?.*)?$/i.test(url)) {
860
- try {
861
- const urlObj = new URL(`http://x${req.url}`);
862
- const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
863
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
864
- res.writeHead(400);
865
- res.end('{}');
866
- return true;
867
- }
868
- const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
869
- if (!q || q.length < 2) {
870
- res.writeHead(200, {
871
- 'Content-Type': 'application/json',
872
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
873
- });
874
- res.end('{"hits":[]}');
875
- return true;
876
- }
877
-
878
- const d = path.resolve(urlObj.searchParams.get('dir') || ctx.projectDir || process.cwd());
879
- const orgsDir = path.join(d, '.monomind', 'orgs');
880
- const readJ = (f) => {
881
- try {
882
- return JSON.parse(fs.readFileSync(f, 'utf8'));
883
- } catch (_) {
884
- return null;
885
- }
886
- };
887
-
888
- const hits = [];
889
- const match = (str) => str?.toLowerCase().includes(q);
890
-
891
- // Agents
892
- const config = readJ(path.join(orgsDir, `${orgName}.json`));
893
- for (const role of config?.roles || []) {
894
- if (
895
- match(role.id) ||
896
- match(role.title) ||
897
- (role.responsibilities || []).some((r) => match(r))
898
- ) {
899
- hits.push({ type: 'agent', id: role.id, title: role.title, meta: role.agent_type });
900
- }
901
- }
902
-
903
- // Goals
904
- const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
905
- for (const g of goals?.goals || []) {
906
- if (match(g.title) || match(g.text) || match(g.goal) || match(g.description)) {
907
- hits.push({
908
- type: 'goal',
909
- id: g.id,
910
- title: g.title || g.text || g.goal,
911
- meta: g.status || 'open',
912
- });
913
- }
914
- }
915
-
916
- // Routines
917
- const routines = readJ(path.join(orgsDir, `${orgName}-routines.json`));
918
- for (const r of routines?.routines || []) {
919
- if (match(r.name) || match(r.description)) {
920
- hits.push({ type: 'routine', id: r.name, title: r.name, meta: r.schedule || '' });
921
- }
922
- }
923
-
924
- // Approvals
925
- for (const a of approvalsOrEmpty(orgsDir, orgName)) {
926
- if (match(a.action) || match(a.roleId)) {
927
- hits.push({
928
- type: 'approval',
929
- id: a.id,
930
- title: `${a.roleId}: ${a.action}`,
931
- meta: a.status,
932
- });
933
- }
934
- }
935
-
936
- // Projects
937
- const projects = readJ(path.join(orgsDir, `${orgName}-projects.json`));
938
- for (const p of projects?.projects || []) {
939
- if (match(p.name) || match(p.description)) {
940
- hits.push({
941
- type: 'project',
942
- id: p.id || p.name,
943
- title: p.name,
944
- meta: p.status || 'active',
945
- });
946
- }
947
- }
948
-
949
- // Issues
950
- const issuesData = readJ(path.join(orgsDir, `${orgName}-issues.json`));
951
- for (const i of issuesData?.issues || []) {
952
- if (match(i.title) || match(i.description) || match(i.slug)) {
953
- hits.push({
954
- type: 'issue',
955
- id: i.id || i.slug,
956
- title: i.title || i.slug,
957
- meta: i.status || 'open',
958
- });
959
- }
960
- }
961
-
962
- // Recent activity events
963
- const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
964
- if (fs.existsSync(eventsFile)) {
965
- const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).slice(-500);
966
- for (const l of lines) {
967
- try {
968
- const e = JSON.parse(l);
969
- if (e.org === orgName && match(JSON.stringify(e))) {
970
- hits.push({
971
- type: 'event',
972
- id: String(e.ts),
973
- title: e.type,
974
- meta: e.role || e.task || '',
975
- });
976
- if (hits.length >= 50) break;
977
- }
978
- } catch (_) {}
979
- }
980
- }
981
-
982
- res.writeHead(200, {
983
- 'Content-Type': 'application/json',
984
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
985
- });
986
- res.end(JSON.stringify({ q, hits: hits.slice(0, 50) }));
987
- } catch (_) {
988
- res.writeHead(500);
989
- res.end('{}');
990
- }
991
- return true;
992
- }
993
-
994
- // GET /api/org/:name/health — aggregate org health, from the same runtime
995
- // sources as /runtime: live role sessions, the task DAG, this run's usage on
996
- // the budget's enforcement basis, and how the last 7 days of runs ended.
997
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/health$/i)) {
998
- const orgName = decodeURIComponent(url.split('/')[3]);
999
- return hilRespond(res, corsOrigin, async () => {
1000
- const v = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
1001
- if (!v) throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
1002
- const running = v.roles.filter((r) => r.status === 'running').length;
1003
- const openTasks = v.tasks.filter((t) =>
1004
- ['pending', 'ready', 'blocked'].includes(t.status),
1005
- ).length;
1006
- const activeTasks = v.tasks.filter((t) => t.status === 'running').length;
1007
- const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
1008
- // A resumed run keeps its run id and appends a history line per stop:
1009
- // count each run once, by its latest line (history is newest first).
1010
- const seenRuns = new Set();
1011
- const week = v.history.filter(
1012
- (h) => (h.endedAt || 0) >= weekAgo && !seenRuns.has(h.run) && seenRuns.add(h.run),
1013
- );
1014
- // Only a boss's gate-checked org_complete with 'achieved' counts as success.
1015
- const achieved = week.filter(
1016
- (h) => h.closedBy === 'org-complete' && h.outcome?.status === 'achieved',
1017
- ).length;
1018
- const { used, tokens } = v.budget;
1019
- return {
1020
- agents_running: running,
1021
- agents_idle: v.roles.length - running,
1022
- agents_active: running,
1023
- open_issues: openTasks,
1024
- in_progress_issues: activeTasks,
1025
- tasks_pending: openTasks + activeTasks,
1026
- budget_used_tokens: used,
1027
- budget_max_tokens: tokens,
1028
- budget_basis: v.budget.basis,
1029
- budget_used_pct: tokens && used !== null ? Math.round((used / tokens) * 100) : null,
1030
- run_success_rate_7d: week.length ? Math.round((achieved / week.length) * 100) : null,
1031
- total_runs_7d: week.length,
1032
- errors: [...v.problems, ...(v.liveError ? [`daemon: ${v.liveError}`] : [])],
1033
- };
1034
- });
1035
- }
1036
-
1037
- // GET /api/org/:name/plugins — plugins from registry filtered/merged with org overrides
1038
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/plugins$/i)) {
1039
- try {
1040
- const orgName = decodeURIComponent(url.split('/')[3]);
1041
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1042
- res.writeHead(400);
1043
- res.end('Invalid org name');
1044
- return true;
1045
- }
1046
- const _pluginsQs = new URL(req.url, 'http://localhost').searchParams;
1047
- const base = path.join(
1048
- path.resolve(_pluginsQs.get('dir') || ctx.projectDir || process.cwd()),
1049
- '.monomind',
1050
- );
1051
- let plugins = [];
1052
- try {
1053
- const reg = JSON.parse(
1054
- fs.readFileSync(path.join(base, 'plugins', 'registry.json'), 'utf8'),
1055
- );
1056
- plugins = reg.plugins || [];
1057
- // Strip sensitive config fields from output
1058
- plugins = plugins.map((p) => {
1059
- const safe = { ...p };
1060
- if (safe.config) {
1061
- safe.config = Object.fromEntries(
1062
- Object.entries(safe.config).map(([k, v]) =>
1063
- /key|token|secret|password|api/i.test(k) ? [k, '***'] : [k, v],
1064
- ),
1065
- );
1066
- }
1067
- return safe;
1068
- });
1069
- } catch (_) {
1070
- /* no global registry */
1071
- }
1072
- // Merge org-level overrides
1073
- try {
1074
- const orgPlugins = JSON.parse(
1075
- fs.readFileSync(path.join(base, 'orgs', `${orgName}-plugins.json`), 'utf8'),
1076
- );
1077
- const overrideMap = {};
1078
- (orgPlugins.plugins || []).forEach((p) => {
1079
- overrideMap[p.id] = p;
1080
- });
1081
- if (Object.keys(overrideMap).length) {
1082
- plugins = plugins.map((p) =>
1083
- overrideMap[p.id] ? { ...p, ...overrideMap[p.id], _orgOverride: true } : p,
1084
- );
1085
- }
1086
- } catch (_) {
1087
- /* no org-level overrides */
1088
- }
1089
- res.writeHead(200, { 'Content-Type': 'application/json' });
1090
- res.end(JSON.stringify({ plugins }));
1091
- } catch (_) {
1092
- res.writeHead(500);
1093
- res.end('{}');
1094
- }
1095
- return true;
1096
- }
1097
-
1098
- // GET /api/org/:name/runtime — what the org is doing now, from the runtime's
1099
- // own sources (live daemon status, run bus, idle watchdog, history).
1100
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runtime$/i)) {
1101
- const orgName = decodeURIComponent(url.split('/')[3]);
1102
- return hilRespond(res, corsOrigin, async () => {
1103
- const view = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
1104
- if (!view)
1105
- throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
1106
- return view;
1107
- });
1108
- }
1109
-
1110
- // POST /api/org/:name/config — edit the org definition's goal, schedule,
1111
- // run_config and default cost tier; refused unless the result passes the
1112
- // same OrgDefSchema parse `org run` does.
1113
- if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/config$/i)) {
1114
- const orgName = decodeURIComponent(url.split('/')[3]);
1115
- const root = hilRoot(req, ctx);
1116
- return hilRespond(res, corsOrigin, async () => {
1117
- const body = await readHilBody(req);
1118
- if (!body || typeof body !== 'object')
1119
- throw Object.assign(new Error('JSON body required'), { status: 400 });
1120
- try {
1121
- return { ok: true, def: orgRuntime.patchOrgConfig(root, orgName, body) };
1122
- } catch (err) {
1123
- if (err instanceof orgRuntime.ConfigRejected)
1124
- throw Object.assign(new Error(err.message), { status: 400 });
1125
- throw err;
1126
- }
1127
- });
1128
- }
1129
-
1130
- // GET /api/org/:name/agents — agents from roles + merged heartbeat state
1131
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agents$/i)) {
1132
- try {
1133
- const orgName = decodeURIComponent(url.split('/')[3]);
1134
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1135
- res.writeHead(400);
1136
- res.end('Invalid org name');
1137
- return true;
1138
- }
1139
- const _agentsQs = new URL(req.url, 'http://localhost').searchParams;
1140
- const d = path.resolve(_agentsQs.get('dir') || ctx.projectDir || process.cwd());
1141
- const base = path.join(d, '.monomind', 'orgs');
1142
- const readJsonSafe = (f) => {
1143
- try {
1144
- return JSON.parse(fs.readFileSync(f, 'utf8'));
1145
- } catch (_) {
1146
- return null;
1147
- }
1148
- };
1149
- const config = readJsonSafe(path.join(base, `${orgName}.json`)) || {};
1150
- const stateData = readJsonSafe(path.join(base, `${orgName}-state.json`)) || {};
1151
- const agentState =
1152
- stateData.agents || stateData.roles
1153
- ? stateData.agents || Object.fromEntries((stateData.roles || []).map((r) => [r.id, r]))
1154
- : {};
1155
- const roles = config.roles || [];
1156
- const agents = roles.map((r) => {
1157
- const s = agentState[r.id] || {};
1158
- // Runtime and model the role actually runs with (cost tiers included).
1159
- const m = orgRuntime.describeRoleModel(r, config);
1160
- return {
1161
- id: r.id,
1162
- title: r.title || r.id,
1163
- adapterType: m.runtime,
1164
- adapterModel: m.model,
1165
- effort: m.effort,
1166
- tier: m.tier,
1167
- governance: r.governance || null,
1168
- reportsTo: r.reports_to || null,
1169
- status: s.status || 'idle',
1170
- lastHeartbeat: s.last_heartbeat || s.lastHeartbeat || null,
1171
- tokensIn: s.tokens_in || 0,
1172
- tokensOut: s.tokens_out || 0,
1173
- cacheRead: s.cache_read_tokens || 0,
1174
- cacheCreation: s.cache_creation_tokens || 0,
1175
- tokensUsed: s.tokens_used || 0,
1176
- costUsd: s.total_cost_usd || 0,
1177
- };
1178
- });
1179
- res.writeHead(200, {
1180
- 'Content-Type': 'application/json',
1181
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1182
- });
1183
- res.end(JSON.stringify({ agents }));
1184
- } catch (_) {
1185
- res.writeHead(500);
1186
- res.end('{"agents":[]}');
1187
- }
1188
- return true;
1189
- }
1190
-
1191
- // GET /api/org/:name/approvals — the org's tool/action approvals
1192
- // (<org>/approvals.json, written by orgrt/approvals.ts), newest first.
1193
- if (
1194
- req.method === 'GET' &&
1195
- url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals(\?.*)?$/i)
1196
- ) {
1197
- const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1198
- return hilRespond(res, corsOrigin, () => {
1199
- const approvals = hil.listApprovals(hilRoot(req, ctx), orgName);
1200
- return { approvals, pending: approvals.filter((a) => a.status === 'pending').length };
1201
- });
1202
- }
1203
-
1204
- // GET /api/org/:name/gates — the org's decision gates (<org>/gates.json).
1205
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates(\?.*)?$/i)) {
1206
- const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1207
- return hilRespond(res, corsOrigin, () => {
1208
- const gates = hil.listGates(hilRoot(req, ctx), orgName);
1209
- return { gates, pending: gates.filter((g) => g.status === 'pending').length };
1210
- });
1211
- }
1212
-
1213
- // GET /api/questions?dir= — ask_human questions (pending and answered) for
1214
- // every org in one project, each with its `blocking` flag.
1215
- if (req.method === 'GET' && (url === '/api/questions' || url.startsWith('/api/questions?'))) {
1216
- return hilRespond(res, corsOrigin, () => ({
1217
- questions: hil.pendingForProject(hilRoot(req, ctx)).questions,
1218
- }));
1219
- }
1220
-
1221
- // GET /api/hil?dir= — everything in one project that waits on a human:
1222
- // questions, approvals and decision gates across all its orgs.
1223
- if (req.method === 'GET' && (url === '/api/hil' || url.startsWith('/api/hil?'))) {
1224
- return hilRespond(res, corsOrigin, () => hil.pendingForProject(hilRoot(req, ctx)));
1225
- }
1226
-
1227
- // POST /api/questions/answer { dir, org, questionId, answer } — live to the
1228
- // hosting daemon (operator credential), else queued for the org's next run.
1229
- if (req.method === 'POST' && url === '/api/questions/answer') {
1230
- return hilRespond(res, corsOrigin, async () => {
1231
- const body = await readHilBody(req);
1232
- const { org, questionId, answer } = body || {};
1233
- if (!validOrgName(org) || !questionId || typeof answer !== 'string' || !answer.trim())
1234
- throw Object.assign(new Error('org, questionId and a non-empty answer are required'), {
1235
- status: 400,
1236
- });
1237
- const root = path.resolve(body.dir || ctx.projectDir || process.cwd());
1238
- const r = await hil.answerQuestion(root, org, String(questionId), answer.trim());
1239
- hilEvent(ctx, root, {
1240
- type: 'org:question-answered',
1241
- org,
1242
- role: r.role,
1243
- questionId,
1244
- delivery: r.delivery,
1245
- });
1246
- return { ok: true, ...r };
1247
- });
1248
- }
1249
-
1250
- // POST /api/org/:name/gates/:id { approved: boolean, resolution? } — resolve
1251
- // a decision gate, live or (org not running) in gates.json.
1252
- if (
1253
- req.method === 'POST' &&
1254
- url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates\/[^/?]+(\?.*)?$/i)
1255
- ) {
1256
- const parts = url.split('?')[0].split('/');
1257
- const orgName = decodeURIComponent(parts[3]);
1258
- const root = hilRoot(req, ctx);
1259
- return hilRespond(res, corsOrigin, async () => {
1260
- const gateId = decodeHilSegment(parts[5]);
1261
- const body = await readHilBody(req);
1262
- const resolution =
1263
- typeof body?.resolution === 'string' && body.resolution.trim()
1264
- ? body.resolution.trim().slice(0, 4000)
1265
- : undefined;
1266
- if (typeof body?.approved !== 'boolean')
1267
- throw Object.assign(new Error('approved (boolean) is required'), { status: 400 });
1268
- const r = await hil.resolveGate(root, orgName, gateId, body.approved, resolution);
1269
- const status = body.approved ? 'approved' : 'rejected';
1270
- hilEvent(ctx, root, {
1271
- type: 'org:gate:resolved',
1272
- org: orgName,
1273
- gateId,
1274
- status,
1275
- delivery: r.delivery,
1276
- });
1277
- return { ok: true, status, ...r };
1278
- });
1279
- }
1280
-
1281
- // POST /api/org/:name/approvals/:requestId { action: "approve" | "reject" } —
1282
- // resolve one tool/action approval, live or (org not running) in approvals.json.
1283
- if (
1284
- req.method === 'POST' &&
1285
- url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/?]+(\?.*)?$/i)
1286
- ) {
1287
- const parts = url.split('?')[0].split('/');
1288
- const orgName = decodeURIComponent(parts[3]);
1289
- const root = hilRoot(req, ctx);
1290
- return hilRespond(res, corsOrigin, async () => {
1291
- const requestId = decodeHilSegment(parts[5]);
1292
- const body = await readHilBody(req);
1293
- if (body?.action !== 'approve' && body?.action !== 'reject')
1294
- throw Object.assign(new Error('action must be approve or reject'), { status: 400 });
1295
- const approved = body.action === 'approve';
1296
- const r = await hil.resolveApproval(root, orgName, requestId, approved);
1297
- const status = approved ? 'approved' : 'denied';
1298
- hilEvent(ctx, root, {
1299
- type: 'org:approval:resolved',
1300
- org: orgName,
1301
- requestId,
1302
- status,
1303
- delivery: r.delivery,
1304
- });
1305
- return { ok: true, status, ...r };
1306
- });
1307
- }
1308
-
1309
- // GET /api/org/:name/budgets — org and per-agent budget data
1310
- // Returns: { org_budget: {limit_tokens, limit_usd}, agent_budgets: {agentId: {limit_usd}}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
1311
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/budgets$/i)) {
1312
- try {
1313
- const orgName = decodeURIComponent(url.split('/')[3]);
1314
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1315
- res.writeHead(400);
1316
- res.end('Invalid org name');
1317
- return true;
1318
- }
1319
- const _budgetsQs = new URL(req.url, 'http://localhost').searchParams;
1320
- const base = path.join(
1321
- path.resolve(_budgetsQs.get('dir') || ctx.projectDir || process.cwd()),
1322
- '.monomind',
1323
- 'orgs',
1324
- );
1325
- let budgetData = { org_budget: {}, agent_budgets: {}, period: 'monthly', currency: 'USD' };
1326
- try {
1327
- budgetData = JSON.parse(
1328
- fs.readFileSync(path.join(base, `${orgName}-budgets.json`), 'utf8'),
1329
- );
1330
- } catch (_) {}
1331
- // Enrich with per-agent spend from state file.
1332
- // State file format: { agents: { "<role_id>": { tokens_in, tokens_out, ... } } }
1333
- let agents = [];
1334
- try {
1335
- const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
1336
- const agentMap = state.agents || {};
1337
- // Also load role titles from org config for enrichment
1338
- const roleMap = {};
1339
- try {
1340
- const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
1341
- (cfg.roles || []).forEach((r) => {
1342
- roleMap[r.id] = r.title || r.id;
1343
- });
1344
- } catch (_) {}
1345
- agents = Object.entries(agentMap).map(([id, s]) => ({
1346
- id,
1347
- title: roleMap[id] || s.title || id,
1348
- tokens_in: s.tokens_in || 0,
1349
- tokens_out: s.tokens_out || 0,
1350
- tokens_used: s.tokens_used || (s.tokens_in || 0) + (s.tokens_out || 0),
1351
- total_cost_usd: s.total_cost_usd || 0,
1352
- }));
1353
- } catch (_) {}
1354
- // Scan org run jsonl files for usage events (fallback when state.json has no token data).
1355
- // Two event-type variants show up here in practice and must BOTH be matched, each with its
1356
- // own field mapping — matching only one silently drops real v2 cost data from the UI:
1357
- // - 'agent:usage': flattened { role, tokens_in, tokens_out, cost_usd } (legacy/direct writers).
1358
- // - 'org:usage': orgrt's actual forwarded shape (attachForwarder's translate() default case
1359
- // for a raw OrgBus 'usage' event) — { from, data: { tokens, cost_usd } }. orgrt never emits
1360
- // 'agent:usage' itself, so scanning for that alone means this fallback never fires for real runs.
1361
- const _hasTokenData = agents.some(
1362
- (a) => a.tokens_in > 0 || a.tokens_out > 0 || a.total_cost_usd > 0,
1363
- );
1364
- if (!_hasTokenData) {
1365
- try {
1366
- const _runsDir = path.join(base, orgName, 'runs');
1367
- if (fs.existsSync(_runsDir)) {
1368
- const _usageByRole = {};
1369
- const _bump = (role, tokensIn, tokensOut, tokensTotal, costUsd) => {
1370
- if (!role) return;
1371
- role = String(role).trim();
1372
- if (!role) return;
1373
- if (!_usageByRole[role])
1374
- _usageByRole[role] = {
1375
- tokens_in: 0,
1376
- tokens_out: 0,
1377
- tokens_used: 0,
1378
- total_cost_usd: 0,
1379
- };
1380
- _usageByRole[role].tokens_in += tokensIn;
1381
- _usageByRole[role].tokens_out += tokensOut;
1382
- _usageByRole[role].tokens_used += tokensIn + tokensOut + tokensTotal;
1383
- _usageByRole[role].total_cost_usd += costUsd;
1384
- };
1385
- for (const f of fs.readdirSync(_runsDir)) {
1386
- if (!f.endsWith('.jsonl') || f.startsWith('._')) continue;
1387
- const lines = fs
1388
- .readFileSync(path.join(_runsDir, f), 'utf8')
1389
- .split('\n')
1390
- .filter(Boolean);
1391
- for (const l of lines) {
1392
- try {
1393
- const ev = JSON.parse(l);
1394
- if (ev.type === 'agent:usage' && ev.role) {
1395
- _bump(
1396
- ev.role,
1397
- Number(ev.tokens_in) || 0,
1398
- Number(ev.tokens_out) || 0,
1399
- 0,
1400
- Number(ev.cost_usd) || 0,
1401
- );
1402
- } else if (ev.type === 'org:usage' && ev.from) {
1403
- _bump(
1404
- ev.from,
1405
- 0,
1406
- 0,
1407
- Number(ev.data?.tokens) || 0,
1408
- Number(ev.data?.cost_usd) || 0,
1409
- );
1410
- }
1411
- } catch (_) {}
1412
- }
1413
- }
1414
- if (Object.keys(_usageByRole).length > 0) {
1415
- // Merge usage into agents list; preserve role titles
1416
- agents = agents.map((a) => {
1417
- const u = _usageByRole[a.id] || {};
1418
- return {
1419
- ...a,
1420
- tokens_in: u.tokens_in || 0,
1421
- tokens_out: u.tokens_out || 0,
1422
- tokens_used: u.tokens_used || (u.tokens_in || 0) + (u.tokens_out || 0),
1423
- total_cost_usd: u.total_cost_usd || 0,
1424
- };
1425
- });
1426
- // Add any roles that appeared in events but aren't in config
1427
- for (const [role, u] of Object.entries(_usageByRole)) {
1428
- if (!agents.find((a) => a.id === role))
1429
- agents.push({ id: role, title: role, ...u });
1430
- }
1431
- }
1432
- }
1433
- } catch (_) {}
1434
- }
1435
- // Do NOT fall back to zero-value role stubs — empty agents array is the honest signal
1436
- // that no usage has been tracked yet; the UI shows "No cost data" rather than $0.0000 rows.
1437
- res.writeHead(200, { 'Content-Type': 'application/json' });
1438
- res.end(JSON.stringify({ ...budgetData, agents }));
1439
- } catch (_) {
1440
- res.writeHead(500);
1441
- res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}');
1442
- }
1443
- return true;
1444
- }
1445
-
1446
- // GET /api/org/:name/threads — conversation threads from threads.jsonl
1447
- // Returns: { threads: [{id, subject, authorId, authorName, issueId, createdAt, messages:[]}] }
1448
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/threads$/i)) {
1449
- try {
1450
- const orgName = decodeURIComponent(url.split('/')[3]);
1451
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1452
- res.writeHead(400);
1453
- res.end('Invalid org name');
1454
- return true;
1455
- }
1456
- const _threadsQs = new URL(req.url, 'http://localhost').searchParams;
1457
- const _threadsRoot = path.resolve(_threadsQs.get('dir') || ctx.projectDir || process.cwd());
1458
- const _threadsProjDir = ctx._resolveOrgProjectDir(orgName, _threadsRoot) || _threadsRoot;
1459
- const threadsFile = path.join(
1460
- _threadsProjDir,
1461
- '.monomind',
1462
- 'orgs',
1463
- `${orgName}-threads.jsonl`,
1464
- );
1465
- let threads = [];
1466
- try {
1467
- const lines = fs
1468
- .readFileSync(threadsFile, 'utf8')
1469
- .split('\n')
1470
- .filter((l) => l.trim());
1471
- threads = lines
1472
- .map((l) => {
1473
- try {
1474
- return JSON.parse(l);
1475
- } catch (_) {
1476
- return null;
1477
- }
1478
- })
1479
- .filter(Boolean);
1480
- // Group 'message' entries (from org:comms) by run_id into synthetic thread objects
1481
- const msgsByRun = {};
1482
- threads
1483
- .filter((t) => t.type === 'message')
1484
- .forEach((m) => {
1485
- const rid = m.run_id || 'unknown';
1486
- if (!msgsByRun[rid])
1487
- msgsByRun[rid] = {
1488
- id: `thread-${rid}`,
1489
- type: 'thread',
1490
- subject: `Run ${rid}`,
1491
- run_id: rid,
1492
- createdAt: m.ts,
1493
- messages: [],
1494
- };
1495
- msgsByRun[rid].messages.push({ from: m.from, to: m.to, msg: m.msg, ts: m.ts });
1496
- });
1497
- const syntheticThreads = Object.values(msgsByRun).map((t) => ({
1498
- ...t,
1499
- messageCount: t.messages.length,
1500
- author: t.messages[0]?.from || null,
1501
- }));
1502
- threads = threads
1503
- .filter((t) => t.type === 'thread' || !t.type)
1504
- .map((t) => ({
1505
- ...t,
1506
- author: t.author || t.authorName || t.createdBy || t.authorId || null,
1507
- messageCount:
1508
- t.messageCount != null
1509
- ? t.messageCount
1510
- : Array.isArray(t.messages)
1511
- ? t.messages.length
1512
- : typeof t.messages === 'number'
1513
- ? t.messages
1514
- : null,
1515
- }));
1516
- threads = [...threads, ...syntheticThreads];
1517
- } catch (_) {}
1518
- res.writeHead(200, { 'Content-Type': 'application/json' });
1519
- res.end(JSON.stringify({ threads }));
1520
- } catch (_) {
1521
- res.writeHead(500);
1522
- res.end('{"threads":[]}');
1523
- }
1524
- return true;
1525
- }
1526
-
1527
- // GET /api/org/:name/routines — read org routines (falls back to synthesizing from org config's loop object)
1528
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
1529
- try {
1530
- const orgName = decodeURIComponent(url.split('/')[3]);
1531
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1532
- res.writeHead(400);
1533
- res.end('Invalid org name');
1534
- return true;
1535
- }
1536
- const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
1537
- const _routinesBase = path.resolve(_routinesQs.get('dir') || ctx.projectDir || process.cwd());
1538
- const _routinesProjDir = ctx._resolveOrgProjectDir(orgName, _routinesBase) || _routinesBase;
1539
- const routinesFile = path.join(
1540
- _routinesProjDir,
1541
- '.monomind',
1542
- 'orgs',
1543
- `${orgName}-routines.json`,
1544
- );
1545
- let data = { routines: [] };
1546
- try {
1547
- data = JSON.parse(fs.readFileSync(routinesFile, 'utf8'));
1548
- } catch (_) {}
1549
- // Synthesize routines from org config's loop/schedule settings when no explicit routines are defined
1550
- if (!data.routines?.length) {
1551
- try {
1552
- const orgCfg = JSON.parse(
1553
- fs.readFileSync(
1554
- path.join(_routinesProjDir, '.monomind', 'orgs', `${orgName}.json`),
1555
- 'utf8',
1556
- ),
1557
- );
1558
- const loop = orgCfg.loop;
1559
- if (loop && (loop.poll_interval_minutes || loop.interval_minutes)) {
1560
- const intervalMin = loop.poll_interval_minutes || loop.interval_minutes;
1561
- data.routines = [
1562
- {
1563
- name: `${orgName}-cycle`,
1564
- description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org iteration cycle',
1565
- schedule: `every ${intervalMin}m`,
1566
- cron: null,
1567
- enabled: orgCfg.status === 'active',
1568
- status: orgCfg.status || 'stopped',
1569
- prompt_file: loop.run_prompt_file || null,
1570
- source: 'loop-config',
1571
- lastRun: null,
1572
- },
1573
- ];
1574
- } else if (orgCfg.schedule) {
1575
- data.routines = [
1576
- {
1577
- name: `${orgName}-schedule`,
1578
- description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org scheduled run',
1579
- schedule: String(orgCfg.schedule),
1580
- cron: null,
1581
- enabled: orgCfg.status === 'active',
1582
- status: orgCfg.status || 'stopped',
1583
- source: 'schedule-config',
1584
- lastRun: null,
1585
- },
1586
- ];
1587
- }
1588
- } catch (_) {}
1589
- }
1590
- res.writeHead(200, {
1591
- 'Content-Type': 'application/json',
1592
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1593
- });
1594
- res.end(JSON.stringify({ routines: data.routines || [] }));
1595
- } catch (_) {
1596
- res.writeHead(500);
1597
- res.end('{"routines":[]}');
1598
- }
1599
- return true;
1600
- }
1601
-
1602
- // POST /api/org/:name/goals — upsert the org goals file
1603
- // Body: { goals: [{id, title, description, status, priority, assignee_id, created_at}] }
1604
- if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/goals$/i)) {
1605
- let body = '';
1606
- for await (const chunk of req) {
1607
- body += chunk;
1608
- if (body.length > 2097152) {
1609
- req.destroy();
1610
- break;
1611
- }
1612
- }
1613
- try {
1614
- const orgName = decodeURIComponent(url.split('/')[3]);
1615
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1616
- res.writeHead(400);
1617
- res.end('Invalid org name');
1618
- return true;
1619
- }
1620
- const parsed = JSON.parse(body);
1621
- if (!parsed || !Array.isArray(parsed.goals)) {
1622
- res.writeHead(400);
1623
- res.end('{"error":"goals array required"}');
1624
- return true;
1625
- }
1626
- const _postGoalsQs = new URL(req.url, 'http://localhost').searchParams;
1627
- const goalsFile = path.join(
1628
- path.resolve(_postGoalsQs.get('dir') || ctx.projectDir || process.cwd()),
1629
- '.monomind',
1630
- 'orgs',
1631
- `${orgName}-goals.json`,
1632
- );
1633
- const tmp = `${goalsFile}.tmp`;
1634
- const payload = { org: orgName, updated_at: new Date().toISOString(), goals: parsed.goals };
1635
- fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
1636
- fs.renameSync(tmp, goalsFile);
1637
- res.writeHead(200, {
1638
- 'Content-Type': 'application/json',
1639
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1640
- });
1641
- res.end(JSON.stringify({ ok: true, count: parsed.goals.length }));
1642
- } catch (_) {
1643
- res.writeHead(500);
1644
- res.end(`{"error":"${String(_).replace(/"/g, '\\"')}"}`);
1645
- }
1646
- return true;
1647
- }
1648
-
1649
- // POST /api/org/:name/routines — upsert the org routines file
1650
- // Body: { routines: [{name, description, schedule, enabled, last_run, next_run}] }
1651
- if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
1652
- let body = '';
1653
- for await (const chunk of req) {
1654
- body += chunk;
1655
- if (body.length > 2097152) {
1656
- req.destroy();
1657
- break;
1658
- }
1659
- }
1660
- try {
1661
- const orgName = decodeURIComponent(url.split('/')[3]);
1662
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1663
- res.writeHead(400);
1664
- res.end('Invalid org name');
1665
- return true;
1666
- }
1667
- const parsed = JSON.parse(body);
1668
- if (!parsed || !Array.isArray(parsed.routines)) {
1669
- res.writeHead(400);
1670
- res.end('{"error":"routines array required"}');
1671
- return true;
1672
- }
1673
- const _postRoutinesQs = new URL(req.url, 'http://localhost').searchParams;
1674
- const routinesFile = path.join(
1675
- path.resolve(_postRoutinesQs.get('dir') || ctx.projectDir || process.cwd()),
1676
- '.monomind',
1677
- 'orgs',
1678
- `${orgName}-routines.json`,
1679
- );
1680
- const tmp = `${routinesFile}.tmp`;
1681
- const payload = {
1682
- org: orgName,
1683
- updated_at: new Date().toISOString(),
1684
- routines: parsed.routines,
1685
- };
1686
- fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
1687
- fs.renameSync(tmp, routinesFile);
1688
- res.writeHead(200, {
1689
- 'Content-Type': 'application/json',
1690
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1691
- });
1692
- res.end(JSON.stringify({ ok: true, count: parsed.routines.length }));
1693
- } catch (_) {
1694
- res.writeHead(500);
1695
- res.end(`{"error":"${String(_).replace(/"/g, '\\"')}"}`);
1696
- }
1697
- return true;
1698
- }
1699
-
1700
- // GET /api/org/:name/files — all files related to an org
1701
- if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/files$/i)) {
1702
- try {
1703
- const orgName = decodeURIComponent(url.split('/')[3]);
1704
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1705
- res.writeHead(400);
1706
- res.end('{"error":"Invalid org name"}');
1707
- return true;
1708
- }
1709
- const _filesQs = new URL(req.url, 'http://localhost').searchParams;
1710
- const d = path.resolve(_filesQs.get('dir') || ctx.projectDir || process.cwd());
1711
- const orgsDir = path.join(d, '.monomind', 'orgs');
1712
- const files = [];
1713
- const seen = new Set();
1714
- const addFile = (fp, type) => {
1715
- if (seen.has(fp)) return;
1716
- seen.add(fp);
1717
- try {
1718
- const st = fs.statSync(fp);
1719
- files.push({
1720
- name: path.basename(fp),
1721
- path: fp,
1722
- type,
1723
- size: st.size,
1724
- mtime: st.mtime.toISOString(),
1725
- });
1726
- } catch (_) {}
1727
- };
1728
- addFile(path.join(orgsDir, `${orgName}.json`), 'config');
1729
- for (const s of [
1730
- '-state',
1731
- '-approvals',
1732
- '-goals',
1733
- '-routines',
1734
- '-projects',
1735
- '-members',
1736
- '-issues',
1737
- '-threads',
1738
- '-budgets',
1739
- ]) {
1740
- const fp = path.join(orgsDir, `${orgName + s}.json`);
1741
- if (fs.existsSync(fp)) addFile(fp, s.slice(1));
1742
- }
1743
- const walkDir = (dir, depth) => {
1744
- if (depth > 3) return;
1745
- let entries;
1746
- try {
1747
- entries = fs.readdirSync(dir, { withFileTypes: true });
1748
- } catch (_) {
1749
- return;
1750
- }
1751
- for (const e of entries) {
1752
- if (e.name.startsWith('.')) continue;
1753
- const fp = path.join(dir, e.name);
1754
- if (e.isDirectory()) walkDir(fp, depth + 1);
1755
- else addFile(fp, 'generated');
1756
- }
1757
- };
1758
- const orgWorkDir = path.join(orgsDir, orgName);
1759
- if (fs.existsSync(orgWorkDir)) walkDir(orgWorkDir, 0);
1760
- let orgCfg = null;
1761
- try {
1762
- orgCfg = JSON.parse(fs.readFileSync(path.join(orgsDir, `${orgName}.json`), 'utf8'));
1763
- } catch (_) {}
1764
- if (orgCfg && Array.isArray(orgCfg.roles)) {
1765
- const agentsDir = path.join(d, '.claude', 'agents');
1766
- const walkAgents = (dir) => {
1767
- let entries;
1768
- try {
1769
- entries = fs.readdirSync(dir, { withFileTypes: true });
1770
- } catch (_) {
1771
- return;
1772
- }
1773
- for (const e of entries) {
1774
- if (e.isDirectory()) {
1775
- walkAgents(path.join(dir, e.name));
1776
- continue;
1777
- }
1778
- if (!e.name.endsWith('.md')) continue;
1779
- const fp = path.join(dir, e.name);
1780
- const base = e.name.replace('.md', '').toLowerCase();
1781
- if (
1782
- orgCfg.roles.some(
1783
- (r) =>
1784
- base === (r.id || '').toLowerCase() ||
1785
- base === (r.agent_type || '').toLowerCase() ||
1786
- (r.instructions_file || '').endsWith(e.name),
1787
- )
1788
- )
1789
- addFile(fp, 'agent-definition');
1790
- }
1791
- };
1792
- if (fs.existsSync(agentsDir)) walkAgents(agentsDir);
1793
- }
1794
- files.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
1795
- res.writeHead(200, {
1796
- 'Content-Type': 'application/json',
1797
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1798
- 'Cache-Control': 'no-cache',
1799
- });
1800
- res.end(JSON.stringify(files));
1801
- } catch (e) {
1802
- res.writeHead(500);
1803
- res.end(JSON.stringify({ error: e.message }));
1804
- }
1805
- return true;
1806
- }
1807
-
1808
- // GET /api/file-content — return raw text content of a .monomind file
1809
- if (req.method === 'GET' && url === '/api/file-content') {
1810
- try {
1811
- const _fcQs = new URL(req.url, 'http://localhost').searchParams;
1812
- const rawPath = _fcQs.get('path');
1813
- const baseDir = path.resolve(_fcQs.get('dir') || ctx.projectDir || process.cwd());
1814
- if (!rawPath) {
1815
- res.writeHead(400);
1816
- res.end('Missing path');
1817
- return true;
1818
- }
1819
- const resolved = path.resolve(rawPath);
1820
- // Security: must be inside .monomind of the project dir
1821
- const monomindDir = path.join(baseDir, '.monomind');
1822
- if (!resolved.startsWith(monomindDir + path.sep) && resolved !== monomindDir) {
1823
- res.writeHead(403);
1824
- res.end('Forbidden');
1825
- return true;
1826
- }
1827
- if (!fs.existsSync(resolved)) {
1828
- res.writeHead(404);
1829
- res.end('Not found');
1830
- return true;
1831
- }
1832
- const stat = fs.statSync(resolved);
1833
- if (!stat.isFile()) {
1834
- res.writeHead(400);
1835
- res.end('Not a file');
1836
- return true;
1837
- }
1838
- if (stat.size > 524288) {
1839
- res.writeHead(413);
1840
- res.end('File too large');
1841
- return true;
1842
- }
1843
- const _fcMime = ctx._detectMimeType(resolved);
1844
- if (_fcMime.startsWith('image/')) {
1845
- res.writeHead(200, {
1846
- 'Content-Type': _fcMime,
1847
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1848
- });
1849
- res.end(fs.readFileSync(resolved));
1850
- return true;
1851
- }
1852
- const content = fs.readFileSync(resolved, 'utf8');
1853
- res.writeHead(200, {
1854
- 'Content-Type': 'text/plain; charset=utf-8',
1855
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1856
- });
1857
- res.end(content);
1858
- } catch (_) {
1859
- res.writeHead(500);
1860
- res.end('Internal error');
1861
- }
1862
- return true;
1863
- }
1864
-
1865
- // DELETE /api/orgs/:name — delete an org config and all associated data files
1866
- if (req.method === 'DELETE' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}(\?.*)?$/i)) {
1867
- try {
1868
- const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1869
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1870
- res.writeHead(400);
1871
- res.end('Invalid org name');
1872
- return true;
1873
- }
1874
- const _delOrgQs = new URL(req.url, 'http://localhost').searchParams;
1875
- const orgsDir = path.join(
1876
- path.resolve(_delOrgQs.get('dir') || ctx.projectDir || process.cwd()),
1877
- '.monomind',
1878
- 'orgs',
1879
- );
1880
- const configFile = path.join(orgsDir, `${orgName}.json`);
1881
- const v1ConfigFile = path.join(orgsDir, `${orgName}.v1.json`);
1882
- if (!fs.existsSync(configFile) && !fs.existsSync(v1ConfigFile)) {
1883
- res.writeHead(404);
1884
- res.end('{"error":"org not found"}');
1885
- return true;
1886
- }
1887
- // Remove all org-associated files (config + state + data)
1888
- try {
1889
- if (fs.existsSync(v1ConfigFile)) fs.unlinkSync(v1ConfigFile);
1890
- } catch (_) {}
1891
- const suffixes = [
1892
- '',
1893
- '-state',
1894
- '-goals',
1895
- '-routines',
1896
- '-approvals',
1897
- '-activity',
1898
- '-issues',
1899
- '-members',
1900
- '-projects',
1901
- '-workspaces',
1902
- '-worktrees',
1903
- '-environments',
1904
- '-plugins',
1905
- '-adapters',
1906
- '-budgets',
1907
- '-threads',
1908
- '-secrets',
1909
- '-join-requests',
1910
- '-bootstrap',
1911
- '-project-workspaces',
1912
- '-approval-comments',
1913
- '-skills',
1914
- ];
1915
- for (const suf of suffixes) {
1916
- const f = path.join(orgsDir, `${orgName}${suf}.json`);
1917
- try {
1918
- if (fs.existsSync(f)) fs.unlinkSync(f);
1919
- } catch (_) {}
1920
- const fjsonl = path.join(orgsDir, `${orgName}${suf}.jsonl`);
1921
- try {
1922
- if (fs.existsSync(fjsonl)) fs.unlinkSync(fjsonl);
1923
- } catch (_) {}
1924
- }
1925
- // Remove stop file if present
1926
- try {
1927
- fs.unlinkSync(path.join(orgsDir, '.stops', `${orgName}.stop`));
1928
- } catch (_) {}
1929
- // Remove org subdirectory under .monomind/orgs/ (legacy flat-file location)
1930
- try {
1931
- const orgWorkDir = path.join(orgsDir, orgName);
1932
- if (fs.existsSync(orgWorkDir)) fs.rmSync(orgWorkDir, { recursive: true, force: true });
1933
- } catch (_) {}
1934
- // Remove org subdirectory under git-safe location (.git/monomind/orgs/<name>/) so run
1935
- // files written by the worktree-aware path (feat 880f034e) are also cleaned up on delete
1936
- try {
1937
- const _delWorkDir = path.resolve(_delOrgQs.get('dir') || ctx.projectDir || process.cwd());
1938
- const _delGitMonoDir = ctx._getGitMonomindDir(_delWorkDir);
1939
- if (_delGitMonoDir) {
1940
- const gitOrgDir = path.join(_delGitMonoDir, 'orgs', orgName);
1941
- if (fs.existsSync(gitOrgDir)) fs.rmSync(gitOrgDir, { recursive: true, force: true });
1942
- }
1943
- } catch (_) {}
1944
- // Remove loop prompt file if present (created for scheduled orgs by createorg)
1945
- try {
1946
- const lpf = path.join(
1947
- path.resolve(ctx.projectDir || process.cwd()),
1948
- '.monomind',
1949
- 'loops',
1950
- `${orgName}.md`,
1951
- );
1952
- if (fs.existsSync(lpf)) fs.unlinkSync(lpf);
1953
- } catch (_) {}
1954
- // Emit org:delete event
1955
- const deleteEvent = { type: 'org:delete', org: orgName, ts: Date.now() };
1956
- ctx
1957
- .appendToFile(
1958
- path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'),
1959
- `${JSON.stringify(deleteEvent)}\n`,
1960
- )
1961
- .catch(() => {});
1962
- ctx.broadcastMm(deleteEvent);
1963
- res.writeHead(200, {
1964
- 'Content-Type': 'application/json',
1965
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1966
- });
1967
- res.end('{"ok":true}');
1968
- } catch (_) {
1969
- res.writeHead(500);
1970
- res.end('{}');
1971
- }
1972
- return true;
1973
- }
1974
-
1975
- // POST /api/orgs/:name/stop — send stop signal to a running org
1976
- if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/stop$/i)) {
1977
- try {
1978
- const orgName = decodeURIComponent(url.split('/')[3]);
1979
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1980
- res.writeHead(400);
1981
- res.end('Invalid org name');
1982
- return true;
1983
- }
1984
- const _stopOrgQs = new URL(req.url, 'http://localhost').searchParams;
1985
- const _stopOrgBase = path.resolve(_stopOrgQs.get('dir') || ctx.projectDir || process.cwd());
1986
- const stopEvent = { type: 'org:stop', org: orgName, ts: Date.now() };
1987
- const dataDir = path.join(_stopOrgBase, 'data');
1988
- try {
1989
- fs.mkdirSync(dataDir, { recursive: true });
1990
- } catch (_) {}
1991
- ctx
1992
- .appendToFile(
1993
- path.join(dataDir, 'mastermind-events.jsonl'),
1994
- `${JSON.stringify(stopEvent)}\n`,
1995
- )
1996
- .catch(() => {});
1997
- // Write stop marker file at the path the REAL stop mechanisms poll:
1998
- // `org run`'s own poll loop and `org serve`'s pollStopfiles() both watch
1999
- // .monomind/orgs/<name>/stop (see clearStopfile/stopAction/pollStopfiles in
2000
- // commands/org.ts) — NOT .monomind/orgs/.stops/<name>.stop, which nothing
2001
- // in the CLI/daemon ever reads.
2002
- try {
2003
- const stopDir = path.join(_stopOrgBase, '.monomind', 'orgs', orgName);
2004
- fs.mkdirSync(stopDir, { recursive: true });
2005
- fs.writeFileSync(path.join(stopDir, 'stop'), new Date().toISOString());
2006
- } catch (_) {}
2007
- ctx.broadcastMm(stopEvent);
2008
- res.writeHead(200, {
2009
- 'Content-Type': 'application/json',
2010
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2011
- });
2012
- res.end('{"ok":true}');
2013
- } catch (_) {
2014
- res.writeHead(500);
2015
- res.end('{}');
2016
- }
2017
- return true;
2018
- }
2019
-
2020
- // POST /api/orgs/:name/copy — copy org config to another project directory
2021
- if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/copy$/i)) {
2022
- let body = '';
2023
- for await (const chunk of req) {
2024
- body += chunk;
2025
- if (body.length > 2097152) {
2026
- req.destroy();
2027
- break;
2028
- }
2029
- }
2030
- try {
2031
- const orgName = decodeURIComponent(url.split('/')[3]);
2032
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
2033
- res.writeHead(400);
2034
- res.end(JSON.stringify({ error: 'Invalid org name' }));
2035
- return true;
2036
- }
2037
- let payload = {};
2038
- try {
2039
- payload = JSON.parse(body);
2040
- } catch (_) {}
2041
- const destination = payload.destination ? String(payload.destination).trim() : '';
2042
- if (!destination) {
2043
- res.writeHead(400);
2044
- res.end(JSON.stringify({ error: 'destination is required' }));
2045
- return true;
2046
- }
2047
- if (!path.isAbsolute(destination)) {
2048
- res.writeHead(400);
2049
- res.end(JSON.stringify({ error: 'destination must be an absolute path' }));
2050
- return true;
2051
- }
2052
- const _copyOrgQs = new URL(req.url, 'http://localhost').searchParams;
2053
- const srcOrgsDir = path.join(
2054
- path.resolve(_copyOrgQs.get('dir') || ctx.projectDir || process.cwd()),
2055
- '.monomind',
2056
- 'orgs',
2057
- );
2058
- const srcFile = path.join(srcOrgsDir, `${orgName}.json`);
2059
- if (!fs.existsSync(srcFile)) {
2060
- res.writeHead(404);
2061
- res.end(JSON.stringify({ error: 'org not found' }));
2062
- return true;
2063
- }
2064
- const destOrgsDir = path.join(path.resolve(destination), '.monomind', 'orgs');
2065
- try {
2066
- fs.mkdirSync(destOrgsDir, { recursive: true });
2067
- } catch (_) {}
2068
- const destFile = path.join(destOrgsDir, `${orgName}.json`);
2069
- fs.copyFileSync(srcFile, destFile);
2070
- res.writeHead(200, {
2071
- 'Content-Type': 'application/json',
2072
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2073
- });
2074
- res.end(JSON.stringify({ ok: true, destFile }));
2075
- } catch (e) {
2076
- res.writeHead(500);
2077
- res.end(JSON.stringify({ error: String(e.message || e) }));
2078
- }
2079
- return true;
2080
- }
2081
-
2082
- // GET /api/org/:name/runs — list structured run files for an org
2083
- if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs(\?.*)?$/i.test(url)) {
2084
- try {
2085
- const _rQs = new URL(req.url, 'http://localhost').searchParams;
2086
- const _rOrgName = decodeURIComponent(url.split('/')[3] || '');
2087
- if (_rOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rOrgName)) {
2088
- res.writeHead(400);
2089
- res.end('{"error":"Invalid org name"}');
2090
- return true;
2091
- }
2092
- const _rExplicitDir = _rQs.get('dir');
2093
- const _rServerRoot = path.resolve(_rExplicitDir || ctx.projectDir || process.cwd());
2094
- // Search across known projects (same logic as /api/orgs) unless explicit dir given
2095
- const _rProjDirs = new Set([_rServerRoot]);
2096
- if (!_rExplicitDir) {
2097
- try {
2098
- const _rKnown = JSON.parse(
2099
- fs.readFileSync(path.join(_rServerRoot, 'data', 'known-projects.json'), 'utf8'),
2100
- );
2101
- _rKnown.forEach((p) => _rProjDirs.add(p));
2102
- } catch (_) {}
2103
- }
2104
- const _rSeenFiles = new Set();
2105
- const runs = [];
2106
- const _parseRun = (filePath, f) => {
2107
- try {
2108
- const raw = fs.readFileSync(filePath, 'utf8');
2109
- const allLines = raw.split('\n').filter(Boolean);
2110
- const parse = (l) => {
2111
- try {
2112
- return JSON.parse(l);
2113
- } catch {
2114
- return null;
2115
- }
2116
- };
2117
- // Merge .warm.jsonl (promoted pre-complete events) for accurate event count + metadata
2118
- const warmFile = filePath.replace(/\.jsonl$/, '.warm.jsonl');
2119
- let warmLines = [];
2120
- let warmEvents = [];
2121
- try {
2122
- if (fs.existsSync(warmFile)) {
2123
- warmLines = fs.readFileSync(warmFile, 'utf8').split('\n').filter(Boolean);
2124
- warmEvents = warmLines.map(parse).filter(Boolean);
2125
- }
2126
- } catch (_) {}
2127
- const combinedLines = [...warmLines, ...allLines];
2128
- const eventCount = combinedLines.length;
2129
- const headEvents = (
2130
- warmEvents.length ? warmEvents : allLines.map(parse).filter(Boolean)
2131
- ).slice(0, 10);
2132
- const _tailEvents = allLines.map(parse).filter(Boolean).slice(-5).length
2133
- ? allLines.map(parse).filter(Boolean).slice(-5)
2134
- : warmEvents.slice(-5);
2135
- const first = headEvents.find((e) => e.type === 'run:start') || headEvents[0];
2136
- const last = [...warmEvents.slice(-5), ...allLines.map(parse).filter(Boolean).slice(-3)]
2137
- .slice()
2138
- .reverse()
2139
- .find((e) => e.type === 'run:complete' || e.type === 'org:complete');
2140
- const cycles = combinedLines.filter((l) => l.includes('"org:checkpoint"')).length;
2141
- const lastEvent =
2142
- allLines.map(parse).filter(Boolean).slice(-1)[0] || warmEvents.slice(-1)[0];
2143
- const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
2144
- const isStale = !last && ageMs > 30 * 60 * 1000;
2145
- const firstBossComms = headEvents.find(
2146
- (e) => e.type === 'org:comms' && (e.from === 'boss' || e.role === 'boss') && e.msg,
2147
- );
2148
- const derivedGoal = first?.goal || firstBossComms?.msg?.slice(0, 80) || '';
2149
- return {
2150
- runId: f.replace(/\.warm\.jsonl$|\.jsonl$/, ''),
2151
- startedAt: first?.ts || 0,
2152
- endedAt: last?.ts || 0,
2153
- status: last ? 'complete' : isStale ? 'stale' : 'running',
2154
- eventCount,
2155
- cycleCount: cycles,
2156
- goal: derivedGoal,
2157
- bossRole: first?.bossRole || '',
2158
- };
2159
- } catch (_) {
2160
- return null;
2161
- }
2162
- };
2163
- // Org Runtime v2 (#238): each run's raw BusEvents live under a
2164
- // per-run directory named for the run itself — orgs/<org>/<runId>/bus.jsonl
2165
- // — never the flat orgs/<org>/runs/<runId>.jsonl layout _parseRun above
2166
- // expects. Translate through the same translate()/companionEvents()
2167
- // forwarder.ts uses for the live view, so historical and live event
2168
- // shapes always match.
2169
- const _parseV2RunDir = (orgDir, runId) => {
2170
- try {
2171
- const busFile = path.join(orgDir, runId, 'bus.jsonl');
2172
- if (!fs.existsSync(busFile)) return null;
2173
- const raw = fs
2174
- .readFileSync(busFile, 'utf8')
2175
- .split('\n')
2176
- .filter(Boolean)
2177
- .map((l) => {
2178
- try {
2179
- return JSON.parse(l);
2180
- } catch {
2181
- return null;
2182
- }
2183
- })
2184
- .filter(Boolean);
2185
- if (!raw.length) return null;
2186
- const translated = raw.flatMap((e) => [...companionEvents(e), translate(e)]);
2187
- const done = translated.find(
2188
- (e) => e.type === 'session:complete' || e.type === 'org:complete',
2189
- );
2190
- const start = translated.find((e) => e.type === 'org:start');
2191
- const firstComms = translated.find((e) => e.type === 'org:comms' && e.msg);
2192
- return {
2193
- runId,
2194
- startedAt: raw[0]?.ts || 0,
2195
- endedAt: done?.ts || 0,
2196
- status: done ? 'complete' : 'running',
2197
- eventCount: raw.length,
2198
- cycleCount: translated.filter((e) => e.type === 'org:checkpoint').length,
2199
- goal: start?.goal || firstComms?.msg?.slice(0, 80) || '',
2200
- bossRole: '',
2201
- };
2202
- } catch (_) {
2203
- return null;
2204
- }
2205
- };
2206
- for (const _rpd of _rProjDirs) {
2207
- // Check both .monomind and .git/monomind locations
2208
- const _rMonoDir = ctx._getGitMonomindDir(_rpd) || path.join(_rpd, '.monomind');
2209
- const _rOrgDirs = [path.join(_rMonoDir, 'orgs', _rOrgName)];
2210
- if (_rMonoDir !== path.join(_rpd, '.monomind'))
2211
- _rOrgDirs.push(path.join(_rpd, '.monomind', 'orgs', _rOrgName));
2212
- for (const _rOrgDir of _rOrgDirs) {
2213
- if (!fs.existsSync(_rOrgDir)) continue;
2214
- const runDirs = fs
2215
- .readdirSync(_rOrgDir)
2216
- .filter(
2217
- (d) => d.startsWith('run-') && fs.statSync(path.join(_rOrgDir, d)).isDirectory(),
2218
- )
2219
- .sort()
2220
- .reverse();
2221
- for (const runId of runDirs.slice(0, 50)) {
2222
- if (_rSeenFiles.has(runId)) continue;
2223
- const r = _parseV2RunDir(_rOrgDir, runId);
2224
- if (!r) continue;
2225
- _rSeenFiles.add(runId);
2226
- runs.push(r);
2227
- }
2228
- }
2229
- const _rSearchDirs = [path.join(_rMonoDir, 'orgs', _rOrgName, 'runs')];
2230
- if (_rMonoDir !== path.join(_rpd, '.monomind'))
2231
- _rSearchDirs.push(path.join(_rpd, '.monomind', 'orgs', _rOrgName, 'runs'));
2232
- for (const _rDir of _rSearchDirs) {
2233
- if (!fs.existsSync(_rDir)) continue;
2234
- // Include .warm.jsonl — completed runs are renamed hot→warm on org:complete and
2235
- // must stay visible in the chat history dropdown.
2236
- const files = fs
2237
- .readdirSync(_rDir)
2238
- .filter(
2239
- (f) =>
2240
- f.endsWith('.jsonl') &&
2241
- !f.startsWith('._') &&
2242
- !f.endsWith('.convs.jsonl') &&
2243
- !f.endsWith('.cold.jsonl'),
2244
- )
2245
- .sort()
2246
- .reverse();
2247
- for (const f of files.slice(0, 50)) {
2248
- const _rFileId = f.replace(/\.warm\.jsonl$|\.jsonl$/, '');
2249
- if (_rSeenFiles.has(_rFileId)) continue;
2250
- _rSeenFiles.add(_rFileId);
2251
- const r = _parseRun(path.join(_rDir, f), f);
2252
- if (r) runs.push(r);
2253
- }
2254
- }
2255
- }
2256
- runs.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
2257
- // Threads fallback: if no run files found, synthesize a run from the org threads file
2258
- // so orgs whose boss never emitted runId-tagged events still show in the chat dropdown.
2259
- if (runs.length === 0) {
2260
- for (const _rpd of _rProjDirs) {
2261
- const _tf = path.join(_rpd, '.monomind', 'orgs', `${_rOrgName}-threads.jsonl`);
2262
- if (!fs.existsSync(_tf)) continue;
2263
- const _tLines = fs.readFileSync(_tf, 'utf8').split('\n').filter(Boolean);
2264
- if (!_tLines.length) continue;
2265
- const _tEvs = _tLines
2266
- .map((l) => {
2267
- try {
2268
- return JSON.parse(l);
2269
- } catch {
2270
- return null;
2271
- }
2272
- })
2273
- .filter(Boolean);
2274
- if (!_tEvs.length) continue;
2275
- const _firstTs = _tEvs[0].ts
2276
- ? typeof _tEvs[0].ts === 'number'
2277
- ? _tEvs[0].ts
2278
- : new Date(_tEvs[0].ts).getTime()
2279
- : Date.now();
2280
- const _lastTs = _tEvs[_tEvs.length - 1].ts
2281
- ? typeof _tEvs[_tEvs.length - 1].ts === 'number'
2282
- ? _tEvs[_tEvs.length - 1].ts
2283
- : new Date(_tEvs[_tEvs.length - 1].ts).getTime()
2284
- : _firstTs;
2285
- runs.push({
2286
- id: `threads-${_rOrgName}`,
2287
- orgName: _rOrgName,
2288
- goal: `${_rOrgName} threads`,
2289
- status: 'complete',
2290
- startedAt: _firstTs,
2291
- endedAt: _lastTs,
2292
- eventCount: _tEvs.length,
2293
- _threadsFile: _tf,
2294
- });
2295
- break;
2296
- }
2297
- }
2298
- res.writeHead(200, {
2299
- 'Content-Type': 'application/json',
2300
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2301
- 'Cache-Control': 'no-cache',
2302
- });
2303
- res.end(JSON.stringify(runs));
2304
- } catch (_) {
2305
- res.writeHead(500);
2306
- res.end('[]');
2307
- }
2308
- return true;
2309
- }
2310
-
2311
- // GET /api/org/:name/runs/:runId — get all events for a specific run
2312
- if (
2313
- req.method === 'GET' &&
2314
- /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/[a-z0-9][a-z0-9_-]{0,79}(\?.*)?$/i.test(url)
2315
- ) {
2316
- try {
2317
- const _rvQs = new URL(req.url, 'http://localhost').searchParams;
2318
- const _rvParts = url.replace(/\?.*$/, '').split('/');
2319
- const _rvOrgName = decodeURIComponent(_rvParts[3] || '');
2320
- const _rvRunId = decodeURIComponent(_rvParts[5] || '');
2321
- if (
2322
- _rvOrgName.length > 64 ||
2323
- !/^[a-z0-9][a-z0-9_-]*$/i.test(_rvOrgName) ||
2324
- _rvRunId.length > 80 ||
2325
- !/^[a-z0-9][a-z0-9_-]*$/i.test(_rvRunId)
2326
- ) {
2327
- res.writeHead(400);
2328
- res.end('{"error":"Invalid org or run id"}');
2329
- return true;
2330
- }
2331
- const _rvExplicitDir = _rvQs.get('dir');
2332
- const _rvServerRoot = path.resolve(_rvExplicitDir || ctx.projectDir || process.cwd());
2333
- // Threads fallback: threads-${orgName} is a synthetic runId served from threads file
2334
- if (_rvRunId === `threads-${_rvOrgName}`) {
2335
- const _rvProjDirsT = new Set([_rvServerRoot]);
2336
- if (!_rvExplicitDir) {
2337
- try {
2338
- JSON.parse(
2339
- fs.readFileSync(path.join(_rvServerRoot, 'data', 'known-projects.json'), 'utf8'),
2340
- ).forEach((p) => _rvProjDirsT.add(p));
2341
- } catch (_) {}
2342
- }
2343
- for (const _rvpd of _rvProjDirsT) {
2344
- const _tf = path.join(_rvpd, '.monomind', 'orgs', `${_rvOrgName}-threads.jsonl`);
2345
- if (!fs.existsSync(_tf)) continue;
2346
- const _tLines = fs.readFileSync(_tf, 'utf8').split('\n').filter(Boolean);
2347
- const _tEvs = _tLines
2348
- .map((l) => {
2349
- try {
2350
- const e = JSON.parse(l);
2351
- return {
2352
- type: 'org:comms',
2353
- from: e.role || e.from || 'agent',
2354
- to: e.to || 'all',
2355
- msg: e.message || e.msg || '',
2356
- ts: e.ts
2357
- ? typeof e.ts === 'number'
2358
- ? e.ts
2359
- : new Date(e.ts).getTime()
2360
- : Date.now(),
2361
- org: _rvOrgName,
2362
- runId: _rvRunId,
2363
- };
2364
- } catch {
2365
- return null;
2366
- }
2367
- })
2368
- .filter(Boolean);
2369
- res.writeHead(200, {
2370
- 'Content-Type': 'application/json',
2371
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2372
- });
2373
- res.end(JSON.stringify(_tEvs));
2374
- return true;
2375
- }
2376
- res.writeHead(404);
2377
- res.end('{"error":"threads file not found"}');
2378
- return true;
2379
- }
2380
- // Search across known projects
2381
- const _rvProjDirs = new Set([_rvServerRoot]);
2382
- if (!_rvExplicitDir) {
2383
- try {
2384
- JSON.parse(
2385
- fs.readFileSync(path.join(_rvServerRoot, 'data', 'known-projects.json'), 'utf8'),
2386
- ).forEach((p) => _rvProjDirs.add(p));
2387
- } catch (_) {}
2388
- }
2389
- // Org Runtime v2 (#238): try the per-run-directory bus.jsonl layout
2390
- // first — orgs/<org>/<runId>/bus.jsonl — translated to the same
2391
- // dashboard vocabulary the live view gets from forwarder.ts, before
2392
- // falling back to the v1 flat orgs/<org>/runs/<runId>.jsonl lookup
2393
- // below (v1 files are already stored pre-translated).
2394
- for (const _rvpd of _rvProjDirs) {
2395
- const _rvMonoDir = ctx._getGitMonomindDir(_rvpd) || path.join(_rvpd, '.monomind');
2396
- const _v2Candidates = [path.join(_rvMonoDir, 'orgs', _rvOrgName, _rvRunId, 'bus.jsonl')];
2397
- if (_rvMonoDir !== path.join(_rvpd, '.monomind'))
2398
- _v2Candidates.push(
2399
- path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, _rvRunId, 'bus.jsonl'),
2400
- );
2401
- for (const _v2File of _v2Candidates) {
2402
- if (!fs.existsSync(_v2File)) continue;
2403
- const raw = fs
2404
- .readFileSync(_v2File, 'utf8')
2405
- .split('\n')
2406
- .filter(Boolean)
2407
- .map((l) => {
2408
- try {
2409
- return JSON.parse(l);
2410
- } catch {
2411
- return null;
2412
- }
2413
- })
2414
- .filter(Boolean);
2415
- const events = raw.flatMap((e) => [...companionEvents(e), translate(e)]);
2416
- events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
2417
- res.writeHead(200, {
2418
- 'Content-Type': 'application/json',
2419
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2420
- 'Cache-Control': 'no-cache',
2421
- });
2422
- res.end(JSON.stringify(events));
2423
- return true;
2424
- }
2425
- }
2426
- let _rvFile = null;
2427
- for (const _rvpd of _rvProjDirs) {
2428
- const _rvMonoDir = ctx._getGitMonomindDir(_rvpd) || path.join(_rvpd, '.monomind');
2429
- const _candidates = [
2430
- path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`),
2431
- path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.warm.jsonl`),
2432
- ];
2433
- if (_rvMonoDir !== path.join(_rvpd, '.monomind')) {
2434
- _candidates.push(
2435
- path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`),
2436
- );
2437
- _candidates.push(
2438
- path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, 'runs', `${_rvRunId}.warm.jsonl`),
2439
- );
2440
- }
2441
- for (const c of _candidates) {
2442
- if (fs.existsSync(c)) {
2443
- _rvFile = c;
2444
- break;
2445
- }
2446
- }
2447
- if (_rvFile) break;
2448
- }
2449
- if (!_rvFile) {
2450
- res.writeHead(404);
2451
- res.end('{"error":"run not found"}');
2452
- return true;
2453
- }
2454
- const _parseLines = (p) => {
2455
- try {
2456
- return fs
2457
- .readFileSync(p, 'utf8')
2458
- .split('\n')
2459
- .filter(Boolean)
2460
- .map((l) => {
2461
- try {
2462
- return JSON.parse(l);
2463
- } catch {
2464
- return null;
2465
- }
2466
- })
2467
- .filter(Boolean);
2468
- } catch {
2469
- return [];
2470
- }
2471
- };
2472
- const events = _parseLines(_rvFile);
2473
- // Merge .warm.jsonl (pre-run:complete events, including org:comms) if it exists.
2474
- // When run:complete fires, the hot .jsonl is renamed to .warm.jsonl so all pre-complete
2475
- // events live there. The current .jsonl then only holds post-complete events (e.g. org:stop).
2476
- const _rvWarmFile = _rvFile.endsWith('.warm.jsonl')
2477
- ? _rvFile
2478
- : _rvFile.replace(/\.jsonl$/, '.warm.jsonl');
2479
- if (_rvWarmFile !== _rvFile && fs.existsSync(_rvWarmFile)) {
2480
- events.push(..._parseLines(_rvWarmFile));
2481
- }
2482
- // For in-progress runs (no .warm.jsonl), org:comms also go to .convs.jsonl (stripped form).
2483
- // They're already in .jsonl as full events, so .convs.jsonl would duplicate — skip it.
2484
- events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
2485
- res.writeHead(200, {
2486
- 'Content-Type': 'application/json',
2487
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2488
- 'Cache-Control': 'no-cache',
2489
- });
2490
- res.end(JSON.stringify(events));
2491
- } catch (_) {
2492
- res.writeHead(500);
2493
- res.end('[]');
2494
- }
2495
- return true;
2496
- }
2497
-
2498
- // GET /api/org/:name/artifact — serve file content for chat "View" button
2499
- if (req.method === 'GET' && /^\/api\/org\/[^/]+\/artifact/.test(url)) {
2500
- try {
2501
- const _artOrgName = decodeURIComponent(url.split('/')[3] || '');
2502
- if (_artOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_artOrgName)) {
2503
- res.writeHead(400);
2504
- res.end(JSON.stringify({ error: 'Invalid org name' }));
2505
- return true;
2506
- }
2507
- const _artQp = new URL(`http://x${req.url}`).searchParams;
2508
- const _rawPath = _artQp.get('path');
2509
- if (!_rawPath) {
2510
- res.writeHead(400);
2511
- res.end(JSON.stringify({ error: 'path required' }));
2512
- return true;
2513
- }
2514
- const _filePath = path.resolve(decodeURIComponent(_rawPath));
2515
- // Security: this endpoint used to allow reads anywhere under the project
2516
- // root or any data/known-projects.json entry — letting any dashboard
2517
- // client read .env, .git/config, or other arbitrary project files.
2518
- // Restrict it, like /api/file-content above, to this org's own
2519
- // .monomind/orgs/<name>/ directory — never outside .monomind.
2520
- const _artBaseDir = path.resolve(_artQp.get('dir') || ctx.projectDir || process.cwd());
2521
- const _artProjDir = ctx._resolveOrgProjectDir(_artOrgName, _artBaseDir) || _artBaseDir;
2522
- const _artMonoDir =
2523
- ctx._getGitMonomindDir(_artProjDir) || path.join(_artProjDir, '.monomind');
2524
- const _artOrgDir = path.join(_artMonoDir, 'orgs', _artOrgName);
2525
- const _safe = _filePath === _artOrgDir || _filePath.startsWith(_artOrgDir + path.sep);
2526
- if (!_safe) {
2527
- res.writeHead(403);
2528
- res.end(JSON.stringify({ error: 'path not allowed' }));
2529
- return true;
2530
- }
2531
- if (!fs.existsSync(_filePath)) {
2532
- res.writeHead(404);
2533
- res.end(JSON.stringify({ error: 'file not found' }));
2534
- return true;
2535
- }
2536
- const _mime = ctx._detectMimeType(_filePath);
2537
- const _size = fs.statSync(_filePath).size;
2538
- // Reject files >2MB to avoid blocking the event loop
2539
- if (_size > 2 * 1024 * 1024) {
2540
- res.writeHead(413);
2541
- res.end(JSON.stringify({ error: 'file too large', size: _size }));
2542
- return true;
2543
- }
2544
- if (!_mime.startsWith('text/') && _mime !== 'application/json') {
2545
- res.writeHead(200, {
2546
- 'Content-Type': 'application/json',
2547
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2548
- });
2549
- res.end(JSON.stringify({ binary: true, mimeType: _mime, size: _size }));
2550
- return true;
2551
- }
2552
- const _content = fs.readFileSync(_filePath, 'utf8');
2553
- res.writeHead(200, {
2554
- 'Content-Type': 'application/json',
2555
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2556
- });
2557
- res.end(JSON.stringify({ content: _content, mimeType: _mime, size: _size }));
2558
- } catch (_e) {
2559
- res.writeHead(500);
2560
- res.end(JSON.stringify({ error: 'read failed' }));
2561
- }
2562
- return true;
2563
- }
2564
-
2565
- // ------------------------------------------------- Mastermind event system
2566
- // POST /api/mastermind/event — ingest event from mastermind skill
2567
- if (req.method === 'POST' && url === '/api/mastermind/event') {
2568
- await ctx.handleMastermindEvent(req, res, corsOrigin);
2569
- return true;
2570
- }
2571
-
2572
- // GET /api/mastermind-stream — SSE for real-time events
2573
- if (req.method === 'GET' && url === '/api/mastermind-stream') {
2574
- res.writeHead(200, {
2575
- 'Content-Type': 'text/event-stream',
2576
- 'Cache-Control': 'no-cache',
2577
- Connection: 'keep-alive',
2578
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2579
- });
2580
- res.write(': connected\n\n');
2581
- ctx.addMmClient(res);
2582
- // Replay last 50 events from disk (use ?project= param if provided)
2583
- try {
2584
- const _sseQp = new URL(`http://x${req.url}`).searchParams;
2585
- const _sseProj = _sseQp.get('project');
2586
- const root2 = _sseProj || ctx.projectDir || process.cwd();
2587
- const evFile = path.join(root2, 'data', 'mastermind-events.jsonl');
2588
- const lines = fs.readFileSync(evFile, 'utf8').trim().split('\n').filter(Boolean).slice(-50);
2589
- for (const l of lines) res.write(`data: ${l}\n\n`);
2590
- } catch (_) {}
2591
- const ka = setInterval(() => {
2592
- try {
2593
- res.write(': ping\n\n');
2594
- } catch (_) {
2595
- clearInterval(ka);
2596
- ctx.removeMmClient(res);
2597
- }
2598
- }, 20000);
2599
- req.on('close', () => {
2600
- ctx.removeMmClient(res);
2601
- clearInterval(ka);
2602
- });
2603
- return true;
2604
- }
2605
-
2606
- // GET /api/mastermind/sessions
2607
- if (req.method === 'GET' && url.startsWith('/api/mastermind/sessions')) {
2608
- try {
2609
- const qp = new URL(`http://x${req.url}`).searchParams;
2610
- const filterProject = qp.get('project');
2611
- const limitParam = Math.min(parseInt(qp.get('limit') || '200', 10) || 200, 500);
2612
- const serverRoot = ctx.projectDir || process.cwd();
2613
- // Collect all project dirs to aggregate
2614
- const projectDirs = new Set([serverRoot]);
2615
- try {
2616
- const known = JSON.parse(
2617
- fs.readFileSync(path.join(serverRoot, 'data', 'known-projects.json'), 'utf8'),
2618
- );
2619
- known.forEach((p) => projectDirs.add(p));
2620
- } catch (_) {}
2621
- let allSessions = [];
2622
- for (const pd of projectDirs) {
2623
- if (filterProject && pd !== filterProject) continue;
2624
- const sessDir = path.join(pd, 'data', 'sessions');
2625
- const indexFile = path.join(sessDir, '_index.json');
2626
- // ── New format: per-session JSONL + _index.json ──
2627
- if (fs.existsSync(indexFile)) {
2628
- try {
2629
- const idx = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
2630
- const top = idx.slice(0, limitParam);
2631
- for (const entry of top) {
2632
- const _sid = String(entry.id || '').trim();
2633
- if (!_sid || !ctx.SESSION_ID_RE.test(_sid)) continue;
2634
- let events = [];
2635
- try {
2636
- const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
2637
- events = jl
2638
- .trim()
2639
- .split('\n')
2640
- .filter(Boolean)
2641
- .map((l) => {
2642
- try {
2643
- return JSON.parse(l);
2644
- } catch {
2645
- return null;
2646
- }
2647
- })
2648
- .filter(Boolean);
2649
- } catch (_) {}
2650
- allSessions.push({ ...entry, events, project: pd });
2651
- }
2652
- } catch (_) {}
2653
- } else {
2654
- // ── Legacy fallback: mastermind-sessions.json ──
2655
- const f = path.join(pd, 'data', 'mastermind-sessions.json');
2656
- if (fs.existsSync(f)) {
2657
- try {
2658
- const s = JSON.parse(fs.readFileSync(f, 'utf8'));
2659
- s.forEach((sess) => {
2660
- if (!sess.project) sess.project = pd;
2661
- });
2662
- allSessions = allSessions.concat(s);
2663
- } catch (_) {}
2664
- }
2665
- }
2666
- }
2667
- allSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
2668
- res.writeHead(200, {
2669
- 'Content-Type': 'application/json',
2670
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2671
- });
2672
- res.end(JSON.stringify(allSessions.slice(0, limitParam)));
2673
- } catch (_) {
2674
- res.writeHead(200);
2675
- res.end('[]');
2676
- }
2677
- return true;
2678
- }
2679
-
2680
- // GET /api/mastermind/session/:id/trace — human-readable markdown trace
2681
- if (req.method === 'GET' && url.match(/^\/api\/mastermind\/session\/[^/]+\/trace$/)) {
2682
- try {
2683
- const sid = url.split('/')[4];
2684
- const sessFile = path.join(
2685
- ctx.projectDir || process.cwd(),
2686
- 'data',
2687
- 'sessions',
2688
- `${sid}.json`,
2689
- );
2690
- let s = null;
2691
- if (fs.existsSync(sessFile)) {
2692
- s = JSON.parse(fs.readFileSync(sessFile, 'utf8'));
2693
- } else {
2694
- const f = path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
2695
- const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
2696
- s = sessions.find((x) => x.id === sid);
2697
- }
2698
- if (!s) {
2699
- res.writeHead(404);
2700
- res.end('Session not found');
2701
- return true;
2702
- }
2703
- const fmt = (ts) => `${new Date(ts).toISOString().replace('T', ' ').slice(0, 19)} UTC`;
2704
- const lines = [
2705
- `# Mastermind Session Trace: ${s.id}`,
2706
- ``,
2707
- `**Prompt:** ${s.prompt || '(none)'}`,
2708
- `**Status:** ${s.status}`,
2709
- `**Started:** ${fmt(s.ts)}`,
2710
- s.endTs ? `**Ended:** ${fmt(s.endTs)}` : '',
2711
- `**Domains:** ${(s.domains || []).join(', ') || '(none yet)'}`,
2712
- ``,
2713
- ];
2714
- for (const ev of s.events || []) {
2715
- const t = fmt(ev.ts);
2716
- if (ev.type === 'session:start')
2717
- lines.push(`\`${t}\` **SESSION START** — prompt: "${ev.prompt || ''}"`);
2718
- else if (ev.type === 'domain:dispatch')
2719
- lines.push(`\`${t}\` **DOMAIN DISPATCH** → \`${ev.domain}\` — ${ev.cmd || ''}`);
2720
- else if (ev.type === 'agent:spawn')
2721
- lines.push(
2722
- `\`${t}\` **AGENT SPAWN** [\`${ev.domain}\`] → agent: \`${ev.agent}\` — ${ev.task || ''}`,
2723
- );
2724
- else if (ev.type === 'intercom')
2725
- lines.push(`\`${t}\` **INTERCOM** \`${ev.from}\` → \`${ev.to}\`: ${ev.msg || ''}`);
2726
- else if (ev.type === 'domain:complete')
2727
- lines.push(
2728
- `\`${t}\` **DOMAIN COMPLETE** [\`${ev.domain}\`] status: ${ev.status}${ev.artifacts?.length ? ` — artifacts: ${ev.artifacts.join(', ')}` : ''}`,
2729
- );
2730
- else if (ev.type === 'session:complete')
2731
- lines.push(
2732
- `\`${t}\` **SESSION COMPLETE** — status: ${ev.status}, domains: ${(ev.domains || []).join(', ')}`,
2733
- );
2734
- else lines.push(`\`${t}\` ${ev.type} ${JSON.stringify(ev)}`);
2735
- }
2736
- res.writeHead(200, {
2737
- 'Content-Type': 'text/plain; charset=utf-8',
2738
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2739
- });
2740
- res.end(lines.join('\n'));
2741
- } catch (_) {
2742
- res.writeHead(500);
2743
- res.end('Error');
2744
- }
2745
- return true;
2746
- }
2747
-
2748
- // GET /api/mastermind/session/:id
2749
- if (req.method === 'GET' && url.startsWith('/api/mastermind/session/')) {
2750
- try {
2751
- const sid = url.slice('/api/mastermind/session/'.length);
2752
- // Check individual session file first
2753
- const sessFile = path.join(
2754
- ctx.projectDir || process.cwd(),
2755
- 'data',
2756
- 'sessions',
2757
- `${sid}.json`,
2758
- );
2759
- if (fs.existsSync(sessFile)) {
2760
- res.writeHead(200, {
2761
- 'Content-Type': 'application/json',
2762
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2763
- });
2764
- res.end(fs.readFileSync(sessFile, 'utf8'));
2765
- return true;
2766
- }
2767
- const f = path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
2768
- const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
2769
- const s = sessions.find((x) => x.id === sid);
2770
- res.writeHead(200, {
2771
- 'Content-Type': 'application/json',
2772
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2773
- });
2774
- res.end(JSON.stringify(s || null));
2775
- } catch (_) {
2776
- res.writeHead(200);
2777
- res.end('null');
2778
- }
2779
- return true;
2780
- }
2781
-
2782
- // -------------------------------------------------------- GET /mastermind
2783
- if (req.method === 'GET' && url === '/mastermind') {
2784
- // Serve local file if present (dev), otherwise fall back to bundled HTML
2785
- const root = ctx.projectDir || process.cwd();
2786
- const htmlPath = path.join(root, 'docs', 'mastermind-diagram.html');
2787
- let html = ctx.MASTERMIND_DIAGRAM_HTML;
2788
- try {
2789
- html = fs.readFileSync(htmlPath, 'utf8');
2790
- } catch (_) {}
2791
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2792
- res.end(html);
2793
- return true;
2794
- }
2795
-
2796
- // ----------------------------------------------------------- GET /orgs
2797
- if (req.method === 'GET' && url === '/orgs') {
2798
- try {
2799
- const htmlPath = path.join(ctx.__dirname, 'orgs.html');
2800
- let html = fs.readFileSync(htmlPath, 'utf8');
2801
- // Inject this process's auth credential the same way GET / does for
2802
- // dashboard.html — orgs.html's fetch() calls hit the same now
2803
- // default-closed /api/* routes and need a way to know the token.
2804
- html = html.replace(
2805
- '<head>',
2806
- `<head>\n<meta name="mm-token" content="${ctx.dashboardAuthValue}">`,
2807
- );
2808
- res.writeHead(200, {
2809
- 'Content-Type': 'text/html; charset=utf-8',
2810
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
2811
- });
2812
- res.end(html);
2813
- } catch (err) {
2814
- res.writeHead(404);
2815
- res.end(`orgs.html not found: ${err.message}`);
2816
- }
2817
- return true;
2818
- }
2819
-
2820
- // ------------------------------------------------ GET /orgs-files.js
2821
- // Files-tab + diff-view script, split out of orgs.html (see orgs.html's
2822
- // script-src comment). Not a generic static handler — this UI serves each
2823
- // sibling asset via its own hardcoded route, same as GET /orgs above.
2824
- if (req.method === 'GET' && url === '/orgs-files.js') {
2825
- try {
2826
- const jsPath = path.join(ctx.__dirname, 'orgs-files.js');
2827
- const js = fs.readFileSync(jsPath, 'utf8');
2828
- res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
2829
- res.end(js);
2830
- } catch (err) {
2831
- res.writeHead(404);
2832
- res.end(`orgs-files.js not found: ${err.message}`);
2833
- }
2834
- return true;
2835
- }
2836
-
2837
- // GET /api/mastermind/loops — list all active loop state files
2838
- if (req.method === 'GET' && url === '/api/mastermind/loops') {
2839
- try {
2840
- const loopsDir = path.join(ctx.projectDir || process.cwd(), '.monomind', 'loops');
2841
- const loops = [];
2842
- if (fs.existsSync(loopsDir)) {
2843
- const files = fs
2844
- .readdirSync(loopsDir)
2845
- .filter((f) => f.endsWith('.json') && !f.includes('-hil'));
2846
- for (const f of files) {
2847
- try {
2848
- const d = JSON.parse(fs.readFileSync(path.join(loopsDir, f), 'utf8'));
2849
- loops.push(d);
2850
- } catch (_) {}
2851
- }
2852
- }
2853
- loops.sort((a, b) => (b.lastRunAt || 0) - (a.lastRunAt || 0));
2854
- res.writeHead(200, { 'Content-Type': 'application/json' });
2855
- res.end(JSON.stringify({ loops }));
2856
- } catch (_) {
2857
- res.writeHead(500);
2858
- res.end('{"loops":[]}');
2859
- }
2860
- return true;
2861
- }
2862
-
2863
- // GET /api/status — live system snapshot for dashboard polling
2864
- if (req.method === 'GET' && url === '/api/status') {
2865
- try {
2866
- const root = ctx.projectDir || process.cwd();
2867
- // Active org runs: { orgName -> runId }
2868
- const orgRuns = {};
2869
- ctx.activeOrgRuns.forEach((runId, org) => {
2870
- orgRuns[org] = runId;
2871
- });
2872
- // Recent events (last 10)
2873
- let recentEvents = [];
2874
- try {
2875
- const evPath = path.join(root, 'data', 'mastermind-events.jsonl');
2876
- const lines = fs
2877
- .readFileSync(evPath, 'utf8')
2878
- .split('\n')
2879
- .filter((l) => l.trim())
2880
- .slice(-10);
2881
- recentEvents = lines
2882
- .map((l) => {
2883
- try {
2884
- return JSON.parse(l);
2885
- } catch (_) {
2886
- return null;
2887
- }
2888
- })
2889
- .filter(Boolean);
2890
- } catch (_) {}
2891
- res.writeHead(200, {
2892
- 'Content-Type': 'application/json',
2893
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
2894
- });
2895
- res.end(
2896
- JSON.stringify({
2897
- ts: Date.now(),
2898
- pid: process.pid,
2899
- uptime: process.uptime(),
2900
- dir: root,
2901
- sseClients: getMmClientCount(),
2902
- activeOrgs: Object.keys(orgRuns).length,
2903
- orgRuns,
2904
- recentEvents,
2905
- }),
2906
- );
2907
- } catch (err) {
2908
- res.writeHead(500, { 'Content-Type': 'application/json' });
2909
- res.end(JSON.stringify({ error: err.message }));
2910
- }
2911
- return true;
2912
- }
2913
-
2914
- // GET /api/orgs/:name/runs/current — events from the active run file for an org
2915
- if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/runs\/current$/.test(url)) {
2916
- try {
2917
- const orgName = decodeURIComponent(url.split('/')[3]);
2918
- const _curQs = new URL(req.url, 'http://localhost').searchParams;
2919
- const root = path.resolve(_curQs.get('dir') || ctx.projectDir || process.cwd());
2920
- // Validate orgName
2921
- if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
2922
- res.writeHead(400);
2923
- res.end('{"error":"invalid org name"}');
2924
- return true;
2925
- }
2926
- const runId = ctx.activeOrgRuns.get(orgName);
2927
- const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
2928
- // Try active run first, then fall back to most recent run file.
2929
- // Org Runtime v2 (#238): the active run's own events live under
2930
- // orgs/<org>/<runId>/bus.jsonl, never the v1 flat orgs/<org>/runs/
2931
- // layout — check that shape (and translate it) before the v1 lookup.
2932
- let runFile = null;
2933
- let isV2 = false;
2934
- if (runId) {
2935
- const v2Candidate = path.join(monoDir, 'orgs', orgName, runId, 'bus.jsonl');
2936
- if (fs.existsSync(v2Candidate)) {
2937
- runFile = v2Candidate;
2938
- isV2 = true;
2939
- } else {
2940
- const candidate = path.join(monoDir, 'orgs', orgName, 'runs', `${runId}.jsonl`);
2941
- if (fs.existsSync(candidate)) runFile = candidate;
2942
- }
2943
- }
2944
- if (!runFile) {
2945
- const orgDir = path.join(monoDir, 'orgs', orgName);
2946
- const runDirs = fs.existsSync(orgDir)
2947
- ? fs
2948
- .readdirSync(orgDir)
2949
- .filter(
2950
- (d) => d.startsWith('run-') && fs.existsSync(path.join(orgDir, d, 'bus.jsonl')),
2951
- )
2952
- .sort()
2953
- .reverse()
2954
- : [];
2955
- if (runDirs.length) {
2956
- runFile = path.join(orgDir, runDirs[0], 'bus.jsonl');
2957
- isV2 = true;
2958
- }
2959
- }
2960
- if (!runFile) {
2961
- const runsDir = path.join(monoDir, 'orgs', orgName, 'runs');
2962
- if (fs.existsSync(runsDir)) {
2963
- const files = fs
2964
- .readdirSync(runsDir)
2965
- .filter((f) => f.endsWith('.jsonl') && !f.startsWith('._'));
2966
- if (files.length) {
2967
- files.sort();
2968
- runFile = path.join(runsDir, files[files.length - 1]);
2969
- }
2970
- }
2971
- }
2972
- if (!runFile) {
2973
- res.writeHead(404);
2974
- res.end('{"events":[],"runId":null}');
2975
- return true;
2976
- }
2977
- const detectedRunId = isV2
2978
- ? path.basename(path.dirname(runFile))
2979
- : path.basename(runFile, '.jsonl');
2980
- const lines = fs
2981
- .readFileSync(runFile, 'utf8')
2982
- .split('\n')
2983
- .filter((l) => l.trim())
2984
- .slice(-100);
2985
- const rawEvents = lines
2986
- .map((l) => {
2987
- try {
2988
- return JSON.parse(l);
2989
- } catch (_) {
2990
- return null;
2991
- }
2992
- })
2993
- .filter(Boolean);
2994
- // v1's stored events are already dashboard-native; v2's raw BusEvents
2995
- // need the same translate()/companionEvents() pass the live view gets.
2996
- const events = isV2
2997
- ? rawEvents.flatMap((e) => [...companionEvents(e), translate(e)])
2998
- : rawEvents;
2999
- res.writeHead(200, {
3000
- 'Content-Type': 'application/json',
3001
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3002
- });
3003
- res.end(
3004
- JSON.stringify({ runId: detectedRunId, events, active: ctx.activeOrgRuns.has(orgName) }),
3005
- );
3006
- } catch (err) {
3007
- res.writeHead(500);
3008
- res.end(JSON.stringify({ error: err.message }));
3009
- }
3010
- return true;
3011
- }
3012
-
3013
- // GET /api/orgs/:name/history — per-run outcome summaries (history.jsonl, newest first)
3014
- if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/history$/.test(url)) {
3015
- try {
3016
- const orgName = decodeURIComponent(url.split('/')[3]);
3017
- const _histQs = new URL(req.url, 'http://localhost').searchParams;
3018
- const root = path.resolve(_histQs.get('dir') || ctx.projectDir || process.cwd());
3019
- if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
3020
- res.writeHead(400);
3021
- res.end('{"error":"invalid org name"}');
3022
- return true;
3023
- }
3024
- const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
3025
- const histFile = path.join(monoDir, 'orgs', orgName, 'history.jsonl');
3026
- let runs = [];
3027
- if (fs.existsSync(histFile)) {
3028
- runs = fs
3029
- .readFileSync(histFile, 'utf8')
3030
- .split('\n')
3031
- .filter((l) => l.trim())
3032
- .map((l) => {
3033
- try {
3034
- return JSON.parse(l);
3035
- } catch (_) {
3036
- return null;
3037
- }
3038
- })
3039
- .filter(Boolean)
3040
- .reverse()
3041
- .slice(0, 50);
3042
- }
3043
- res.writeHead(200, {
3044
- 'Content-Type': 'application/json',
3045
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3046
- });
3047
- res.end(JSON.stringify({ runs }));
3048
- } catch (err) {
3049
- res.writeHead(500);
3050
- res.end(JSON.stringify({ error: err.message }));
3051
- }
3052
- return true;
3053
- }
3054
-
3055
- // GET /api/orgs/:name/memory — org knowledge-graph stats + glossary + rules
3056
- if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/memory$/.test(url)) {
3057
- try {
3058
- const orgName = decodeURIComponent(url.split('/')[3]);
3059
- const _memQs = new URL(req.url, 'http://localhost').searchParams;
3060
- const root = path.resolve(_memQs.get('dir') || ctx.projectDir || process.cwd());
3061
- if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
3062
- res.writeHead(400);
3063
- res.end('{"error":"invalid org name"}');
3064
- return true;
3065
- }
3066
- const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
3067
- const dbPath = path.join(monoDir, 'org-memory');
3068
- if (!fs.existsSync(dbPath)) {
3069
- res.writeHead(200, {
3070
- 'Content-Type': 'application/json',
3071
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3072
- });
3073
- res.end(JSON.stringify({ nodes: 0, edges: 0, rules: 0, glossary: [], ruleTexts: [] }));
3074
- return true;
3075
- }
3076
- const kg = await import('../memory/memory-kg.js');
3077
- const [stats, glossary, ruleList] = await Promise.all([
3078
- kg.kgStats({ dbPath }),
3079
- kg.kgGlossary({ dbPath, limit: 12 }),
3080
- kg.kgListRules({ dbPath, limit: 10 }),
3081
- ]);
3082
- res.writeHead(200, {
3083
- 'Content-Type': 'application/json',
3084
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3085
- });
3086
- res.end(
3087
- JSON.stringify({
3088
- ...stats,
3089
- glossary,
3090
- ruleTexts: ruleList.map((r) => r.rule.slice(0, 200)),
3091
- }),
3092
- );
3093
- } catch (err) {
3094
- res.writeHead(500);
3095
- res.end(JSON.stringify({ error: err.message }));
3096
- }
3097
- return true;
3098
- }
3099
-
3100
- // POST /api/knowledge/search — warm semantic knowledge search for hooks.
3101
- // The dashboard server is the one long-lived process on the machine, so it
3102
- // holds the local embedding model warm; per-prompt hook subprocesses (which
3103
- // cannot afford a model load) POST here and fall back to keyword scoring
3104
- // when this endpoint is cold/absent. Fully local — model + store on disk.
3105
- if (req.method === 'POST' && url === '/api/knowledge/search') {
3106
- let body = '';
3107
- let overLimit = false;
3108
- req.on('data', (c) => {
3109
- if (overLimit) return;
3110
- body += c;
3111
- if (body.length > 64 * 1024) {
3112
- // queries are prompts, not documents
3113
- overLimit = true;
3114
- res.writeHead(413, { 'Content-Type': 'application/json' });
3115
- res.end('{"error":"request body too large"}');
3116
- req.destroy();
3117
- }
3118
- });
3119
- req.on('end', async () => {
3120
- if (overLimit) return;
3121
- try {
3122
- const payload = JSON.parse(body || '{}');
3123
- const query = String(payload.query || '').slice(0, 2000);
3124
- const limit = Math.min(Math.max(parseInt(payload.limit, 10) || 3, 1), 10);
3125
- const namespace =
3126
- typeof payload.namespace === 'string' && /^[A-Za-z0-9:_-]{1,128}$/.test(payload.namespace)
3127
- ? payload.namespace
3128
- : 'knowledge:shared';
3129
- if (!query) {
3130
- res.writeHead(400, { 'Content-Type': 'application/json' });
3131
- res.end('{"error":"query required"}');
3132
- return;
3133
- }
3134
- const bridge = await ctx._getKnowledgeBridge();
3135
- if (!bridge) {
3136
- res.writeHead(503, { 'Content-Type': 'application/json' });
3137
- res.end('{"error":"knowledge bridge unavailable"}');
3138
- return;
3139
- }
3140
- // scope: project | global | all (default all) — project results get a
3141
- // small tie boost; global hits are flagged so callers can show origin.
3142
- const scope =
3143
- payload.scope === 'project' || payload.scope === 'global' ? payload.scope : 'all';
3144
- // Rule-based router (no LLM): chunks always run (this endpoint's
3145
- // bread and butter); the auxiliary surfaces — distilled rules,
3146
- // knowledge-graph triplets, pattern memories — only spend a query
3147
- // when the router votes for them or routing is unconfident.
3148
- let wantRules = true,
3149
- wantKg = true,
3150
- wantMemory = true;
3151
- let rrfFuse = null;
3152
- try {
3153
- const routerMod = await import('../memory/query-router.js');
3154
- const route = routerMod.routeQuery(query);
3155
- rrfFuse = routerMod.rrfFuse;
3156
- wantRules = !route.confident || route.surfaces.includes('rules');
3157
- wantKg = !route.confident || route.surfaces.includes('kg');
3158
- wantMemory = !route.confident || route.surfaces.includes('memory');
3159
- } catch {
3160
- /* router unavailable — keep all surfaces on */
3161
- }
3162
- let kgSearch = null;
3163
- if (wantKg && scope !== 'global') {
3164
- try {
3165
- kgSearch = (await import('../memory/memory-kg.js')).kgSearch;
3166
- } catch {
3167
- /* kg unavailable */
3168
- }
3169
- }
3170
- // Superseded-version filtering — the SAME rule `searchKnowledge` (and
3171
- // therefore `monomind doc search` / `knowledge_search`) applies. This
3172
- // endpoint queries the bridge directly for warmth, so without this it
3173
- // would inject old document versions the Second Brain itself hides.
3174
- let liveProj = new Set(),
3175
- liveGlob = new Set(),
3176
- isSuperseded = () => false,
3177
- overfetch = (n) => n;
3178
- // metaProj/metaGlob distinguish "no metadata log" (cannot judge, keep
3179
- // everything) from "log exists and is empty" (every document was
3180
- // removed, keep nothing). Without them, `doc remove` of the last
3181
- // document left its chunks being injected into every prompt.
3182
- let metaProj = false,
3183
- metaGlob = false;
3184
- try {
3185
- const dp = await import('../knowledge/document-pipeline.js');
3186
- // CLAUDE_PROJECT_DIR is whatever directory the user opened, which
3187
- // can be a package subdirectory, while the CLI writes the metadata
3188
- // log at the project root. Walk up to the nearest one that has it:
3189
- // resolving the wrong root yields an empty live set, which silently
3190
- // turns filtering OFF — and superseded rows are the large majority
3191
- // of a long-lived store, so injection would be dominated by stale
3192
- // document versions.
3193
- const resolveKnowledgeRoot = (start) => {
3194
- let probe = path.resolve(start);
3195
- for (let up = 0; up < 8; up++) {
3196
- if (dp.hasKnowledgeMetadata?.(probe)) return probe;
3197
- const parent = path.dirname(probe);
3198
- if (parent === probe) break;
3199
- probe = parent;
3200
- }
3201
- return path.resolve(start);
3202
- };
3203
- const projRoot = resolveKnowledgeRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
3204
- const globRoot =
3205
- process.env.MONOMIND_GLOBAL_BRAIN_DIR ||
3206
- path.join(os.homedir(), '.monomind', 'global-brain');
3207
- liveProj = dp.liveContentHashes(projRoot);
3208
- liveGlob = dp.liveContentHashes(globRoot);
3209
- metaProj = dp.hasKnowledgeMetadata?.(projRoot) ?? liveProj.size > 0;
3210
- metaGlob = dp.hasKnowledgeMetadata?.(globRoot) ?? liveGlob.size > 0;
3211
- isSuperseded = dp.isSupersededKey;
3212
- overfetch = dp.supersededOverfetchLimit;
3213
- } catch {
3214
- /* pipeline unavailable — no filtering, same as before */
3215
- }
3216
- const keepLive = (rows, live, hasMeta) =>
3217
- (rows || [])
3218
- .filter((r) => !isSuperseded(String(r.key || ''), live, hasMeta))
3219
- .slice(0, limit);
3220
- const [projRaw, globRaw, rules, graph, mems] = await Promise.all([
3221
- scope !== 'global'
3222
- ? bridge
3223
- .bridgeSearchEntries({ query, namespace, limit: overfetch(limit, liveProj) })
3224
- .catch(() => null)
3225
- : null,
3226
- scope !== 'project'
3227
- ? bridge
3228
- .bridgeSearchEntries({
3229
- query,
3230
- namespace: 'knowledge:global',
3231
- limit: overfetch(limit, liveGlob),
3232
- dbPath: '@global',
3233
- })
3234
- .catch(() => null)
3235
- : null,
3236
- // Distilled rules ("when X do Y") learned from sessions/runs —
3237
- // small namespace, high injection value, threshold keeps it quiet.
3238
- scope !== 'global' && wantRules
3239
- ? bridge
3240
- .bridgeSearchEntries({ query, namespace: 'rules', limit: 2, threshold: 0.45 })
3241
- .catch(() => null)
3242
- : null,
3243
- // Knowledge-graph triplets: relationship-shaped queries surface
3244
- // facts no chunk contains. Triplet scores derive from seeded
3245
- // cosine matches (≥0.35), so they clear callers' relevance floors.
3246
- scope !== 'global' && kgSearch ? kgSearch({ query, limit: 4 }).catch(() => null) : null,
3247
- // Past patterns/decisions for "last time / previously" queries.
3248
- scope !== 'global' && wantMemory
3249
- ? bridge
3250
- .bridgeSearchEntries({ query, namespace: 'patterns', limit: 2, threshold: 0.4 })
3251
- .catch(() => null)
3252
- : null,
3253
- ]);
3254
- const lists = [
3255
- keepLive(projRaw?.results, liveProj, metaProj).map((r) => ({
3256
- id: r.id,
3257
- key: r.key,
3258
- content: String(r.content || '').slice(0, 2000),
3259
- score: r.score + 0.05,
3260
- global: false,
3261
- tags: r.tags,
3262
- importance: 0.6,
3263
- })),
3264
- keepLive(globRaw?.results, liveGlob, metaGlob).map((r) => ({
3265
- id: r.id,
3266
- key: r.key,
3267
- content: String(r.content || '').slice(0, 2000),
3268
- score: r.score,
3269
- global: true,
3270
- tags: r.tags,
3271
- })),
3272
- (rules?.results || []).map((r) => ({
3273
- id: r.id,
3274
- key: r.key,
3275
- content: String(r.content || '').slice(0, 2000),
3276
- score: r.score + 0.05,
3277
- global: false,
3278
- rule: true,
3279
- tags: r.tags,
3280
- importance: 0.7,
3281
- })),
3282
- (graph?.triplets || []).map((t, i) => ({
3283
- id: `kg:${i}:${t.source}|${t.relation}|${t.target}`,
3284
- key: `kg:${t.source}`,
3285
- content: String(
3286
- t.source +
3287
- ' —' +
3288
- t.relation +
3289
- '→ ' +
3290
- t.target +
3291
- (t.fact && t.fact !== `${t.source} ${t.relation} ${t.target}`
3292
- ? ` (${String(t.fact).slice(0, 300)})`
3293
- : ''),
3294
- ).slice(0, 2000),
3295
- score: t.score,
3296
- global: false,
3297
- triplet: true,
3298
- })),
3299
- (mems?.results || []).map((r) => ({
3300
- id: r.id,
3301
- key: r.key,
3302
- content: String(r.content || '').slice(0, 2000),
3303
- score: r.score,
3304
- global: false,
3305
- memory: true,
3306
- tags: r.tags,
3307
- })),
3308
- ];
3309
- // Rank-fuse across surfaces (raw scores aren't comparable between
3310
- // cosine chunks and blended triplets); each item keeps its native
3311
- // score so downstream relevance floors still apply. Fallback: flat
3312
- // score sort when the router module failed to load.
3313
- const merged = (
3314
- rrfFuse
3315
- ? rrfFuse(lists, limit)
3316
- : lists
3317
- .flat()
3318
- .sort((a, b) => b.score - a.score)
3319
- .slice(0, limit)
3320
- ).map(({ importance, rrf, ...r }) => r);
3321
- // Served excerpts are (very likely) injected into the caller's prompt —
3322
- // that IS usage. Reinforce frequency_weight, per-store, fire-and-forget.
3323
- try {
3324
- const projIds = merged.filter((m) => !m.global && !m.triplet && m.id).map((m) => m.id);
3325
- const globIds = merged.filter((m) => m.global && m.id).map((m) => m.id);
3326
- if (projIds.length) bridge.bridgeRecordUsage?.({ entryIds: projIds }).catch(() => {});
3327
- if (globIds.length)
3328
- bridge.bridgeRecordUsage?.({ entryIds: globIds, dbPath: '@global' }).catch(() => {});
3329
- } catch {
3330
- /* best effort */
3331
- }
3332
- res.writeHead(200, {
3333
- 'Content-Type': 'application/json',
3334
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3335
- });
3336
- res.end(
3337
- JSON.stringify({
3338
- method:
3339
- projRaw?.searchMethod ||
3340
- globRaw?.searchMethod ||
3341
- rules?.searchMethod ||
3342
- mems?.searchMethod ||
3343
- (merged.length ? 'mixed' : 'none'),
3344
- results: merged,
3345
- }),
3346
- );
3347
- } catch (err) {
3348
- res.writeHead(500, { 'Content-Type': 'application/json' });
3349
- res.end(JSON.stringify({ error: err.message }));
3350
- }
3351
- });
3352
- return true;
3353
- }
3354
-
3355
- // GET /api/mastermind/metrics — aggregate system metrics from token-summary and monoswarm-activity
3356
- if (req.method === 'GET' && url === '/api/mastermind/metrics') {
3357
- try {
3358
- const base = path.join(ctx.projectDir || process.cwd(), '.monomind', 'metrics');
3359
- let tokens = {},
3360
- monoswarm = {},
3361
- events = [];
3362
- try {
3363
- tokens = JSON.parse(fs.readFileSync(path.join(base, 'token-summary.json'), 'utf8'));
3364
- } catch (_) {}
3365
- try {
3366
- monoswarm = JSON.parse(fs.readFileSync(path.join(base, 'monoswarm-activity.json'), 'utf8'));
3367
- } catch (_) {}
3368
- try {
3369
- const evPath = path.join(
3370
- ctx.projectDir || process.cwd(),
3371
- 'data',
3372
- 'mastermind-events.jsonl',
3373
- );
3374
- const lines = fs
3375
- .readFileSync(evPath, 'utf8')
3376
- .split('\n')
3377
- .filter((l) => l.trim())
3378
- .slice(-20);
3379
- events = lines
3380
- .map((l) => {
3381
- try {
3382
- return JSON.parse(l);
3383
- } catch (_) {
3384
- return null;
3385
- }
3386
- })
3387
- .filter(Boolean);
3388
- } catch (_) {}
3389
- res.writeHead(200, { 'Content-Type': 'application/json' });
3390
- res.end(JSON.stringify({ tokens, monoswarm, recentEvents: events }));
3391
- } catch (_) {
3392
- res.writeHead(500);
3393
- res.end('{"tokens":{},"monoswarm":{},"recentEvents":[]}');
3394
- }
3395
- return true;
3396
- }
3397
-
3398
- // ------------------------------------------------- GET /api/playbooks
3399
- // List playbook definitions from <dir>/.monomind/playbooks/*.json.
3400
- // (Previously only POST was registered, so GET /api/playbooks?dir=... fell
3401
- // through to the 404 handler.)
3402
- if (req.method === 'GET' && url === '/api/playbooks') {
3403
- try {
3404
- const qp = new URL(req.url, 'http://localhost').searchParams;
3405
- const dir = qp.get('dir') || ctx.projectDir || process.cwd();
3406
- const playbookDir = path.join(path.resolve(dir), '.monomind', 'playbooks');
3407
- const result = [];
3408
- if (fs.existsSync(playbookDir)) {
3409
- const files = fs
3410
- .readdirSync(playbookDir)
3411
- .filter((f) => f.endsWith('.json') && !f.startsWith('._'));
3412
- for (const file of files) {
3413
- try {
3414
- const fpath = path.join(playbookDir, file);
3415
- const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
3416
- const stat = fs.statSync(fpath);
3417
- result.push({
3418
- ...def,
3419
- id: def.id || file.replace('.json', ''),
3420
- file,
3421
- modifiedAt: stat.mtimeMs,
3422
- });
3423
- } catch (_) {}
3424
- }
3425
- }
3426
- res.writeHead(200, {
3427
- 'Content-Type': 'application/json',
3428
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3429
- 'Cache-Control': 'no-cache',
3430
- });
3431
- res.end(JSON.stringify(result));
3432
- } catch (e) {
3433
- res.writeHead(500, { 'Content-Type': 'application/json' });
3434
- res.end(JSON.stringify({ error: e.message }));
3435
- }
3436
- return true;
3437
- }
3438
-
3439
- // ------------------------------------------------- POST /api/playbooks
3440
- // Save a playbook definition to .monomind/playbooks/<id>.json
3441
- if (req.method === 'POST' && url === '/api/playbooks') {
3442
- try {
3443
- let body = '';
3444
- await new Promise((resolve, reject) => {
3445
- req.on('data', (d) => {
3446
- body += d;
3447
- });
3448
- req.on('end', resolve);
3449
- req.on('error', reject);
3450
- });
3451
- const pb = JSON.parse(body);
3452
- if (!pb.id || !pb.name) {
3453
- res.writeHead(400, { 'Content-Type': 'application/json' });
3454
- res.end(JSON.stringify({ error: 'id and name are required' }));
3455
- return true;
3456
- }
3457
- const dir = ctx.projectDir || process.cwd();
3458
- const playbookDir = path.join(dir, '.monomind', 'playbooks');
3459
- fs.mkdirSync(playbookDir, { recursive: true });
3460
- const filePath = path.join(playbookDir, `${pb.id}.json`);
3461
- fs.writeFileSync(filePath, JSON.stringify(pb, null, 2));
3462
- res.writeHead(200, { 'Content-Type': 'application/json' });
3463
- res.end(JSON.stringify({ ok: true, id: pb.id, file: `${pb.id}.json` }));
3464
- } catch (e) {
3465
- res.writeHead(500);
3466
- res.end(JSON.stringify({ error: e.message }));
3467
- }
3468
- return true;
3469
- }
3470
-
3471
- // ------------------------------------------------- GET /api/workflow-defs
3472
- if (req.method === 'GET' && url === '/api/workflow-defs') {
3473
- try {
3474
- const qp = new URL(req.url, 'http://x').searchParams;
3475
- const dir = qp.get('dir') || ctx.projectDir || process.cwd();
3476
- const playbookDir = path.join(dir, '.monomind', 'playbooks');
3477
- const result = [];
3478
- if (fs.existsSync(playbookDir)) {
3479
- const files = fs.readdirSync(playbookDir).filter((f) => f.endsWith('.json'));
3480
- for (const file of files) {
3481
- try {
3482
- const fpath = path.join(playbookDir, file);
3483
- const stat = fs.statSync(fpath);
3484
- const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
3485
- const params = (def.params || []).map((p) =>
3486
- typeof p === 'string' ? p : p.name || p.key || '',
3487
- );
3488
- result.push({
3489
- id: def.id || file.replace('.json', ''),
3490
- name: def.name || file.replace('.json', ''),
3491
- description: def.description || null,
3492
- file,
3493
- nodeCount: Array.isArray(def.nodes) ? def.nodes.length : 0,
3494
- params,
3495
- modifiedAt: stat.mtimeMs,
3496
- });
3497
- } catch (_) {}
3498
- }
3499
- }
3500
- res.writeHead(200, { 'Content-Type': 'application/json' });
3501
- res.end(JSON.stringify(result));
3502
- } catch (e) {
3503
- res.writeHead(500);
3504
- res.end(JSON.stringify({ error: e.message }));
3505
- }
3506
- return true;
3507
- }
3508
-
3509
- // ------------------------------------------------- GET /api/org-coverage
3510
- // Audit coverage report written by the audit loop to .monomind/audit/coverage.json
3511
- if (req.method === 'GET' && url === '/api/org-coverage') {
3512
- try {
3513
- const qp = new URL(req.url, 'http://localhost').searchParams;
3514
- const dir = path.resolve(qp.get('dir') || ctx.projectDir || process.cwd());
3515
- const covPath = path.join(dir, '.monomind', 'audit', 'coverage.json');
3516
- if (!fs.existsSync(covPath)) {
3517
- res.writeHead(404, {
3518
- 'Content-Type': 'application/json',
3519
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3520
- });
3521
- res.end('{}');
3522
- return true;
3523
- }
3524
- const coverage = JSON.parse(fs.readFileSync(covPath, 'utf8'));
3525
- res.writeHead(200, {
3526
- 'Content-Type': 'application/json',
3527
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3528
- 'Cache-Control': 'no-cache',
3529
- });
3530
- res.end(JSON.stringify(coverage));
3531
- } catch (e) {
3532
- res.writeHead(500, { 'Content-Type': 'application/json' });
3533
- res.end(JSON.stringify({ error: e.message }));
3534
- }
3535
- return true;
3536
- }
3537
-
3538
- // ------------------------------------------------- GET /api/workflow-runs
3539
- if (req.method === 'GET' && url === '/api/workflow-runs') {
3540
- // Reads from ~/.monomind/browse-runs.json written by the monobrowse dashboard server.
3541
- try {
3542
- const runsFile = path.join(os.homedir(), '.monomind', 'browse-runs.json');
3543
- if (fs.existsSync(runsFile)) {
3544
- const raw = fs.readFileSync(runsFile, 'utf-8');
3545
- const runs = JSON.parse(raw);
3546
- res.writeHead(200, { 'Content-Type': 'application/json' });
3547
- res.end(JSON.stringify(Array.isArray(runs) ? runs : []));
3548
- } else {
3549
- res.writeHead(200, { 'Content-Type': 'application/json' });
3550
- res.end('[]');
3551
- }
3552
- } catch {
3553
- res.writeHead(200, { 'Content-Type': 'application/json' });
3554
- res.end('[]');
3555
- }
3556
- return true;
3557
- }
3558
-
3559
- // ---- POST /api/orgs/:name/mark-complete — manual STALE recovery ----
3560
- if (
3561
- req.method === 'POST' &&
3562
- /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/mark-complete$/i.test(url)
3563
- ) {
3564
- const _mcOrgName = decodeURIComponent(url.split('/')[3]);
3565
- if (_mcOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_mcOrgName)) {
3566
- res.writeHead(400, { 'Content-Type': 'application/json' });
3567
- res.end(JSON.stringify({ error: 'Invalid org name' }));
3568
- return true;
3569
- }
3570
- const _mcRoot = ctx.projectDir || process.cwd();
3571
- const _mcMonoDir = ctx._getGitMonomindDir(_mcRoot) || path.join(_mcRoot, '.monomind');
3572
- const _mcRunId = ctx.activeOrgRuns.get(_mcOrgName) || ctx._getActiveRunId(_mcOrgName, _mcRoot);
3573
- if (!_mcRunId) {
3574
- res.writeHead(404, { 'Content-Type': 'application/json' });
3575
- res.end(JSON.stringify({ error: `No active run for org: ${_mcOrgName}` }));
3576
- return true;
3577
- }
3578
- const _mcEvent = {
3579
- type: 'run:complete',
3580
- org: _mcOrgName,
3581
- runId: _mcRunId,
3582
- ts: Date.now(),
3583
- reason: 'manual',
3584
- };
3585
- try {
3586
- const _mcRunFile = path.join(_mcMonoDir, 'orgs', _mcOrgName, 'runs', `${_mcRunId}.jsonl`);
3587
- if (fs.existsSync(_mcRunFile))
3588
- await ctx.appendToFile(_mcRunFile, `${JSON.stringify(_mcEvent)}\n`);
3589
- ctx.activeOrgRuns.delete(_mcOrgName);
3590
- // Clean up ppid-keyed active-run files for this org
3591
- const _mcCapDir = path.join(ctx.MONOMIND_HOME, '.monomind', 'capture');
3592
- try {
3593
- const _mcPpidDir = path.join(_mcCapDir, 'active-runs');
3594
- if (fs.existsSync(_mcPpidDir)) {
3595
- fs.readdirSync(_mcPpidDir)
3596
- .filter((f) => f.endsWith('.json'))
3597
- .forEach((_pf) => {
3598
- try {
3599
- const _pd = JSON.parse(fs.readFileSync(path.join(_mcPpidDir, _pf), 'utf8'));
3600
- if (_pd.org === _mcOrgName) fs.unlinkSync(path.join(_mcPpidDir, _pf));
3601
- } catch (_) {}
3602
- });
3603
- }
3604
- const _mcActiveFile = path.join(_mcCapDir, 'active-run.json');
3605
- if (fs.existsSync(_mcActiveFile)) {
3606
- try {
3607
- const _a = JSON.parse(fs.readFileSync(_mcActiveFile, 'utf8'));
3608
- if (_a.org === _mcOrgName) fs.unlinkSync(_mcActiveFile);
3609
- } catch (_) {}
3610
- }
3611
- } catch (_) {}
3612
- ctx._updateRunState(_mcEvent, _mcRoot);
3613
- ctx.broadcastMm(_mcEvent);
3614
- const _mcFwdClients = ctx.runStreamClients.get(_mcOrgName);
3615
- if (_mcFwdClients && _mcFwdClients.size > 0) {
3616
- const _mcLine = `data: ${JSON.stringify(_mcEvent)}\n\n`;
3617
- for (const _cl of _mcFwdClients) {
3618
- try {
3619
- _cl.write(_mcLine);
3620
- } catch (_) {
3621
- _mcFwdClients.delete(_cl);
3622
- }
3623
- }
3624
- }
3625
- res.writeHead(200, {
3626
- 'Content-Type': 'application/json',
3627
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3628
- });
3629
- res.end(JSON.stringify({ ok: true, runId: _mcRunId }));
3630
- } catch (e) {
3631
- res.writeHead(500, { 'Content-Type': 'application/json' });
3632
- res.end(JSON.stringify({ error: e.message }));
3633
- }
3634
- return true;
3635
- }
3636
-
3637
- // ---- GET /api/orgs/:name/runs/current/stream — Phase 3 streaming tail ----
3638
- if (
3639
- req.method === 'GET' &&
3640
- /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/current\/stream$/i.test(url)
3641
- ) {
3642
- const _stOrgName = decodeURIComponent(url.split('/')[3]);
3643
- if (_stOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_stOrgName)) {
3644
- res.writeHead(400);
3645
- res.end('Invalid org name');
3646
- return true;
3647
- }
3648
- const _stQs = new URL(req.url, 'http://localhost').searchParams;
3649
- const _stSince = Math.max(0, parseInt(_stQs.get('since') || '0', 10) || 0);
3650
- res.writeHead(200, {
3651
- 'Content-Type': 'text/event-stream',
3652
- 'Cache-Control': 'no-cache',
3653
- Connection: 'keep-alive',
3654
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3655
- 'X-Accel-Buffering': 'no',
3656
- });
3657
- res.write(': connected\n\n');
3658
- // Register client for live events
3659
- if (!ctx.runStreamClients.has(_stOrgName)) ctx.runStreamClients.set(_stOrgName, new Set());
3660
- ctx.runStreamClients.get(_stOrgName).add(res);
3661
- // Replay events since `since` (SQLite row id cursor; falls back to JSONL line offset)
3662
- try {
3663
- let _stReplayedViaSqlite = false;
3664
- if (ctx._runDb) {
3665
- // SQLite path: cursor is last row id seen (client sends 0 on first connect)
3666
- const _stStmt = ctx._runDb.prepare(
3667
- 'SELECT id, raw FROM run_events WHERE org=? AND id > ? ORDER BY id LIMIT 2000',
3668
- );
3669
- _stStmt.bind([_stOrgName, _stSince]);
3670
- let _stLastId = _stSince;
3671
- while (_stStmt.step()) {
3672
- const _stRow = _stStmt.getAsObject();
3673
- try {
3674
- res.write(`data: ${_stRow.raw}\n\n`);
3675
- _stLastId = _stRow.id;
3676
- } catch (_) {
3677
- break;
3678
- }
3679
- }
3680
- _stStmt.free();
3681
- if (_stLastId > _stSince) {
3682
- // SQLite had rows — send cursor and skip JSONL fallback
3683
- res.write(
3684
- `data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLastId })}\n\n`,
3685
- );
3686
- _stReplayedViaSqlite = true;
3687
- }
3688
- }
3689
- if (!_stReplayedViaSqlite) {
3690
- // JSONL fallback: SQLite absent or returned 0 rows — read directly from run file.
3691
- // `since` is a 0-based line offset in this path.
3692
- const _stRoot = ctx.projectDir || process.cwd();
3693
- const _stRunId =
3694
- ctx.activeOrgRuns.get(_stOrgName) || ctx._getActiveRunId(_stOrgName, _stRoot);
3695
- if (_stRunId) {
3696
- const _stMono = ctx._getGitMonomindDir(_stRoot) || path.join(_stRoot, '.monomind');
3697
- const _stRunFile = path.join(_stMono, 'orgs', _stOrgName, 'runs', `${_stRunId}.jsonl`);
3698
- if (fs.existsSync(_stRunFile)) {
3699
- const _stLines = fs.readFileSync(_stRunFile, 'utf8').trim().split('\n').filter(Boolean);
3700
- for (let _i = _stSince; _i < _stLines.length; _i++) {
3701
- try {
3702
- res.write(`data: ${_stLines[_i]}\n\n`);
3703
- } catch (_) {
3704
- break;
3705
- }
3706
- }
3707
- res.write(
3708
- `data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLines.length })}\n\n`,
3709
- );
3710
- } else {
3711
- res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: 0 })}\n\n`);
3712
- }
3713
- } else {
3714
- res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: 0 })}\n\n`);
3715
- }
3716
- }
3717
- } catch (_) {}
3718
- const _stKa = setInterval(() => {
3719
- try {
3720
- res.write(': ping\n\n');
3721
- } catch (_) {
3722
- clearInterval(_stKa);
3723
- }
3724
- }, 20000);
3725
- req.on('close', () => {
3726
- clearInterval(_stKa);
3727
- const _stClients = ctx.runStreamClients.get(_stOrgName);
3728
- if (_stClients) {
3729
- _stClients.delete(res);
3730
- if (_stClients.size === 0) ctx.runStreamClients.delete(_stOrgName);
3731
- }
3732
- });
3733
- return true;
3734
- }
3735
-
3736
- // ---- POST /api/orgs/:name/chat — user sends a message to a running org ----
3737
- if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/chat$/i.test(url)) {
3738
- let _chBody = '';
3739
- for await (const chunk of req) {
3740
- _chBody += chunk;
3741
- if (_chBody.length > 65536) {
3742
- req.destroy();
3743
- break;
3744
- }
3745
- }
3746
- try {
3747
- const _chOrgName = decodeURIComponent(url.split('/')[3]);
3748
- if (_chOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_chOrgName)) {
3749
- res.writeHead(400, { 'Content-Type': 'application/json' });
3750
- res.end(JSON.stringify({ error: 'Invalid org name' }));
3751
- return true;
3752
- }
3753
- let _chPayload = {};
3754
- try {
3755
- _chPayload = JSON.parse(_chBody);
3756
- } catch (_) {}
3757
- const _chText = String(_chPayload.text || '')
3758
- .trim()
3759
- .slice(0, 4096);
3760
- if (!_chText) {
3761
- res.writeHead(400, { 'Content-Type': 'application/json' });
3762
- res.end(JSON.stringify({ error: 'text is required' }));
3763
- return true;
3764
- }
3765
- const _chQs = new URL(req.url, 'http://localhost').searchParams;
3766
- const _chServerRoot = path.resolve(_chQs.get('dir') || ctx.projectDir || process.cwd());
3767
- const _chRoot = ctx._resolveOrgProjectDir(_chOrgName, _chServerRoot) || _chServerRoot;
3768
- const _chMonoDir = ctx._getGitMonomindDir(_chRoot) || path.join(_chRoot, '.monomind');
3769
- const _chRunId =
3770
- ctx.activeOrgRuns.get(_chOrgName) || ctx._getActiveRunId(_chOrgName, _chRoot);
3771
- // Deliver to the target role (default: the root role) — live into its
3772
- // mailbox, or queued in the org's inbox for its next run.
3773
- let _chRoles = [];
3774
- try {
3775
- const _chCfg = JSON.parse(
3776
- fs.readFileSync(path.join(_chRoot, '.monomind', 'orgs', `${_chOrgName}.json`), 'utf8'),
3777
- );
3778
- if (Array.isArray(_chCfg.roles)) _chRoles = _chCfg.roles;
3779
- } catch (_) {}
3780
- const _chTargetRole =
3781
- typeof _chPayload.role === 'string' && _chPayload.role
3782
- ? _chRoles.find((r) => r.id === _chPayload.role)?.id
3783
- : (_chRoles.find((r) => !r.reports_to) || _chRoles[0])?.id;
3784
- if (!_chTargetRole) {
3785
- res.writeHead(404, { 'Content-Type': 'application/json' });
3786
- res.end(
3787
- JSON.stringify({
3788
- ok: false,
3789
- error: _chPayload.role
3790
- ? `org "${_chOrgName}" has no role "${_chPayload.role}"`
3791
- : `org "${_chOrgName}" has no role to receive it`,
3792
- }),
3793
- );
3794
- return true;
3795
- }
3796
- let _chDelivery;
3797
- try {
3798
- _chDelivery = (await hil.sendHumanMessage(_chRoot, _chOrgName, _chTargetRole, _chText))
3799
- .delivery;
3800
- } catch (err) {
3801
- const { status, error } = hil.hilErrorStatus(err);
3802
- res.writeHead(status, { 'Content-Type': 'application/json' });
3803
- res.end(JSON.stringify({ ok: false, error }));
3804
- return true;
3805
- }
3806
- const _chEvent = {
3807
- type: 'user:message',
3808
- org: _chOrgName,
3809
- runId: _chRunId || null,
3810
- text: _chText,
3811
- ts: Date.now(),
3812
- };
3813
- // Write to run JSONL if active run exists
3814
- if (_chRunId) {
3815
- const _chRunFile = path.join(_chMonoDir, 'orgs', _chOrgName, 'runs', `${_chRunId}.jsonl`);
3816
- if (fs.existsSync(_chRunFile))
3817
- await ctx.appendToFile(_chRunFile, `${JSON.stringify(_chEvent)}\n`);
3818
- }
3819
- // Broadcast to SSE stream clients
3820
- ctx.broadcastMm(_chEvent);
3821
- const _chFwdClients = ctx.runStreamClients.get(_chOrgName);
3822
- if (_chFwdClients && _chFwdClients.size > 0) {
3823
- const _chLine = `data: ${JSON.stringify(_chEvent)}\n\n`;
3824
- for (const _cl of _chFwdClients) {
3825
- try {
3826
- _cl.write(_chLine);
3827
- } catch (_) {
3828
- _chFwdClients.delete(_cl);
3829
- }
3830
- }
3831
- }
3832
- res.writeHead(200, {
3833
- 'Content-Type': 'application/json',
3834
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3835
- });
3836
- res.end(
3837
- JSON.stringify({
3838
- ok: true,
3839
- delivered: _chDelivery === 'live',
3840
- delivery: _chDelivery,
3841
- role: _chTargetRole,
3842
- }),
3843
- );
3844
- } catch (e) {
3845
- res.writeHead(500, { 'Content-Type': 'application/json' });
3846
- res.end(JSON.stringify({ error: e.message }));
3847
- }
3848
- return true;
3849
- }
16
+ if (await handleOrgConfigRoutes(req, res, url, corsOrigin, ctx)) return true;
17
+ if (await handleOrgAgentRoutes(req, res, url, corsOrigin, ctx)) return true;
18
+ if (await handleOrgStatusRoutes(req, res, url, corsOrigin, ctx)) return true;
19
+ if (await handleOrgApprovalRoutes(req, res, url, corsOrigin, ctx)) return true;
20
+ if (await handleOrgPlanningRoutes(req, res, url, corsOrigin, ctx)) return true;
21
+ if (await handleOrgFileRoutes(req, res, url, corsOrigin, ctx)) return true;
22
+ if (await handleOrgLifecycleRoutes(req, res, url, corsOrigin, ctx)) return true;
23
+ if (await handleOrgRunRoutes(req, res, url, corsOrigin, ctx)) return true;
24
+ if (await handleOrgMastermindRoutes(req, res, url, corsOrigin, ctx)) return true;
25
+ if (await handleOrgLiveRoutes(req, res, url, corsOrigin, ctx)) return true;
26
+ if (await handleOrgKnowledgeRoutes(req, res, url, corsOrigin, ctx)) return true;
27
+ if (await handleOrgWorkflowRoutes(req, res, url, corsOrigin, ctx)) return true;
28
+ if (await handleOrgControlRoutes(req, res, url, corsOrigin, ctx)) return true;
3850
29
  return false;
3851
30
  }