@monoes/monomindcli 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  2. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  3. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  4. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  5. package/dist/src/commands/doctor-project-checks.d.ts +12 -1
  6. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  7. package/dist/src/commands/doctor-project-checks.js +55 -4
  8. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  9. package/dist/src/commands/doctor.d.ts.map +1 -1
  10. package/dist/src/commands/doctor.js +10 -1
  11. package/dist/src/commands/doctor.js.map +1 -1
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/init.js +17 -0
  14. package/dist/src/commands/init.js.map +1 -1
  15. package/dist/src/commands/mcp.d.ts.map +1 -1
  16. package/dist/src/commands/mcp.js +4 -3
  17. package/dist/src/commands/mcp.js.map +1 -1
  18. package/dist/src/commands/org-observe.d.ts.map +1 -1
  19. package/dist/src/commands/org-observe.js +14 -0
  20. package/dist/src/commands/org-observe.js.map +1 -1
  21. package/dist/src/commands/org.d.ts.map +1 -1
  22. package/dist/src/commands/org.js +12 -2
  23. package/dist/src/commands/org.js.map +1 -1
  24. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  25. package/dist/src/init/claudemd-generator.js +2 -1
  26. package/dist/src/init/claudemd-generator.js.map +1 -1
  27. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  28. package/dist/src/init/mcp-generator.js +3 -3
  29. package/dist/src/init/mcp-generator.js.map +1 -1
  30. package/dist/src/init/types.d.ts +6 -0
  31. package/dist/src/init/types.d.ts.map +1 -1
  32. package/dist/src/init/types.js.map +1 -1
  33. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  34. package/dist/src/init/write-runtime-config.js +12 -2
  35. package/dist/src/init/write-runtime-config.js.map +1 -1
  36. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  37. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  38. package/dist/src/orgrt/agent-runner.js +47 -0
  39. package/dist/src/orgrt/agent-runner.js.map +1 -1
  40. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  41. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  42. package/dist/src/orgrt/checkpoint.js +15 -6
  43. package/dist/src/orgrt/checkpoint.js.map +1 -1
  44. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  45. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  46. package/dist/src/orgrt/completion-gate.js +45 -0
  47. package/dist/src/orgrt/completion-gate.js.map +1 -1
  48. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  49. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  50. package/dist/src/orgrt/cost-tier.js +183 -0
  51. package/dist/src/orgrt/cost-tier.js.map +1 -0
  52. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  53. package/dist/src/orgrt/cross-org.js +14 -0
  54. package/dist/src/orgrt/cross-org.js.map +1 -1
  55. package/dist/src/orgrt/daemon.d.ts +12 -1
  56. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  57. package/dist/src/orgrt/daemon.js +216 -49
  58. package/dist/src/orgrt/daemon.js.map +1 -1
  59. package/dist/src/orgrt/decisions.d.ts +14 -2
  60. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  61. package/dist/src/orgrt/decisions.js +216 -10
  62. package/dist/src/orgrt/decisions.js.map +1 -1
  63. package/dist/src/orgrt/file-roots.d.ts +7 -0
  64. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  65. package/dist/src/orgrt/file-roots.js +38 -1
  66. package/dist/src/orgrt/file-roots.js.map +1 -1
  67. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  68. package/dist/src/orgrt/forwarder.js +16 -2
  69. package/dist/src/orgrt/forwarder.js.map +1 -1
  70. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  71. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  72. package/dist/src/orgrt/idle-deadline.js +52 -2
  73. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  74. package/dist/src/orgrt/loadouts.d.ts +38 -0
  75. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  76. package/dist/src/orgrt/loadouts.js +129 -0
  77. package/dist/src/orgrt/loadouts.js.map +1 -0
  78. package/dist/src/orgrt/mailbox.d.ts +17 -1
  79. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  80. package/dist/src/orgrt/mailbox.js +50 -3
  81. package/dist/src/orgrt/mailbox.js.map +1 -1
  82. package/dist/src/orgrt/policy.d.ts +42 -2
  83. package/dist/src/orgrt/policy.d.ts.map +1 -1
  84. package/dist/src/orgrt/policy.js +53 -8
  85. package/dist/src/orgrt/policy.js.map +1 -1
  86. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  87. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  88. package/dist/src/orgrt/prompt-vars.js +49 -0
  89. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  90. package/dist/src/orgrt/questions.d.ts +22 -1
  91. package/dist/src/orgrt/questions.d.ts.map +1 -1
  92. package/dist/src/orgrt/questions.js +24 -3
  93. package/dist/src/orgrt/questions.js.map +1 -1
  94. package/dist/src/orgrt/review-packet.d.ts +29 -0
  95. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  96. package/dist/src/orgrt/review-packet.js +67 -0
  97. package/dist/src/orgrt/review-packet.js.map +1 -0
  98. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  99. package/dist/src/orgrt/role-sandbox.js +16 -3
  100. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  101. package/dist/src/orgrt/session-ledger.d.ts +61 -0
  102. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  103. package/dist/src/orgrt/session-ledger.js +113 -0
  104. package/dist/src/orgrt/session-ledger.js.map +1 -0
  105. package/dist/src/orgrt/session.d.ts +45 -5
  106. package/dist/src/orgrt/session.d.ts.map +1 -1
  107. package/dist/src/orgrt/session.js +384 -27
  108. package/dist/src/orgrt/session.js.map +1 -1
  109. package/dist/src/orgrt/task-dag.d.ts +34 -1
  110. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  111. package/dist/src/orgrt/task-dag.js +58 -7
  112. package/dist/src/orgrt/task-dag.js.map +1 -1
  113. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  114. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  115. package/dist/src/orgrt/tool-spill.js +180 -0
  116. package/dist/src/orgrt/tool-spill.js.map +1 -0
  117. package/dist/src/orgrt/types.d.ts +138 -0
  118. package/dist/src/orgrt/types.d.ts.map +1 -1
  119. package/dist/src/orgrt/types.js +148 -0
  120. package/dist/src/orgrt/types.js.map +1 -1
  121. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  122. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  123. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  124. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  125. package/dist/src/protocol-capabilities.d.ts +2 -1
  126. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  127. package/dist/src/protocol-capabilities.js +2 -1
  128. package/dist/src/protocol-capabilities.js.map +1 -1
  129. package/dist/src/ui/dashboard.html +450 -164
  130. package/dist/src/ui/org-hil.mjs +276 -0
  131. package/dist/src/ui/org-runtime.mjs +350 -0
  132. package/dist/src/ui/orgs.html +15 -4
  133. package/dist/src/ui/routes-org.mjs +290 -541
  134. package/dist/src/ui/server.mjs +25 -5
  135. package/dist/tsconfig.tsbuildinfo +1 -1
  136. package/package.json +6 -5
@@ -1,4 +1,3 @@
1
- import { spawn } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import os from 'node:os';
4
3
  import path from 'node:path';
@@ -9,8 +8,70 @@ import path from 'node:path';
9
8
  // companionEvents() are pure — no filesystem or process side effects — so
10
9
  // importing them here doesn't pull in attachForwarder's spawn/heal logic.
11
10
  import { companionEvents, translate } from '../orgrt/forwarder.js';
11
+ import * as hil from './org-hil.mjs';
12
+ import * as orgRuntime from './org-runtime.mjs';
12
13
  import { getMmClientCount } from './sse-manager.mjs';
13
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
+ /** JSON body (≤64KB); null when absent or malformed. */
33
+ async function readHilBody(req) {
34
+ let body = '';
35
+ for await (const chunk of req) {
36
+ body += chunk;
37
+ if (body.length > 65536) {
38
+ req.destroy();
39
+ return null;
40
+ }
41
+ }
42
+ try {
43
+ return JSON.parse(body || '{}');
44
+ } catch (_) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /** Run `fn` and answer with its JSON result, or the HIL error's status. */
50
+ async function hilRespond(res, corsOrigin, fn) {
51
+ const cors = corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {};
52
+ try {
53
+ const data = await fn();
54
+ res.writeHead(200, { 'Content-Type': 'application/json', ...cors });
55
+ res.end(JSON.stringify(data));
56
+ } catch (err) {
57
+ const { status, error } = err.status
58
+ ? { status: err.status, error: err.message }
59
+ : hil.hilErrorStatus(err);
60
+ res.writeHead(status, { 'Content-Type': 'application/json', ...cors });
61
+ res.end(JSON.stringify({ ok: false, error }));
62
+ }
63
+ return true;
64
+ }
65
+
66
+ /** Record a human decision in the dashboard's event log and push it to live views. */
67
+ function hilEvent(ctx, root, ev) {
68
+ const event = { ...ev, resolvedBy: hil.DASHBOARD_RESOLVER, ts: Date.now() };
69
+ ctx
70
+ .appendToFile(path.join(root, 'data', 'mastermind-events.jsonl'), `${JSON.stringify(event)}\n`)
71
+ .catch(() => {});
72
+ ctx.broadcastMm(event);
73
+ }
74
+
14
75
  export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
15
76
  // ------------------------------------------------- Org management
16
77
  // GET /api/orgs — list all saved org configs
@@ -229,9 +290,7 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
229
290
  const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || {
230
291
  routines: [],
231
292
  };
232
- const approvalsData = readJsonSafe(path.join(orgsDir, `${orgName}-approvals.json`)) || {
233
- approvals: [],
234
- };
293
+ const approvalsData = { approvals: approvalsOrEmpty(orgsDir, orgName) };
235
294
 
236
295
  // Check running status: stop file absence AND (in-memory ctx.activeOrgRuns OR state-file agents OR active loop file)
237
296
  // Path must match what `org run`'s poll loop and `org serve`'s pollStopfiles()
@@ -432,14 +491,12 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
432
491
  msg: String(g.text || g.title || g.goal || '').slice(0, 80),
433
492
  }),
434
493
  );
435
- const appr = readJ(path.join(orgsDir, `${orgName}-approvals.json`));
436
- (appr?.approvals || []).forEach((a) => {
437
- const ts = typeof a.ts === 'number' ? a.ts : Date.parse(a.created_at || a.ts || '') || null;
494
+ approvalsOrEmpty(orgsDir, orgName).forEach((a) => {
438
495
  events.push({
439
496
  type: 'approval',
440
- ts,
441
- role: a.agent_id || a.requester || '',
442
- msg: String(a.title || a.action || '').slice(0, 80),
497
+ ts: a.ts,
498
+ role: a.roleId || '',
499
+ msg: `${a.action} — ${a.status}`.slice(0, 80),
443
500
  });
444
501
  });
445
502
  const state = readJ(path.join(orgsDir, `${orgName}-state.json`));
@@ -913,10 +970,14 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
913
970
  }
914
971
 
915
972
  // Approvals
916
- const approvals = readJ(path.join(orgsDir, `${orgName}-approvals.json`));
917
- for (const a of approvals?.approvals || []) {
918
- if (match(a.title) || match(a.action) || match(a.agent_id)) {
919
- hits.push({ type: 'approval', id: a.id, title: a.title, meta: a.status });
973
+ for (const a of approvalsOrEmpty(orgsDir, orgName)) {
974
+ if (match(a.action) || match(a.roleId)) {
975
+ hits.push({
976
+ type: 'approval',
977
+ id: a.id,
978
+ title: `${a.roleId}: ${a.action}`,
979
+ meta: a.status,
980
+ });
920
981
  }
921
982
  }
922
983
 
@@ -1019,97 +1080,42 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
1019
1080
  return true;
1020
1081
  }
1021
1082
 
1022
- // GET /api/org/:name/health — aggregate org health metrics
1083
+ // GET /api/org/:name/health — aggregate org health, from the same runtime
1084
+ // sources as /runtime: live role sessions, the task DAG, this run's usage on
1085
+ // the budget's enforcement basis, and how the last 7 days of runs ended.
1023
1086
  if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/health$/i)) {
1024
- try {
1025
- const orgName = decodeURIComponent(url.split('/')[3]);
1026
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1027
- res.writeHead(400);
1028
- res.end('Invalid org name');
1029
- return true;
1030
- }
1031
- const _healthQs = new URL(req.url, 'http://localhost').searchParams;
1032
- const base = path.join(
1033
- path.resolve(_healthQs.get('dir') || ctx.projectDir || process.cwd()),
1034
- '.monomind',
1035
- 'orgs',
1036
- );
1037
-
1038
- let agentsRunning = 0,
1039
- agentsIdle = 0,
1040
- openIssues = 0,
1041
- inProgressIssues = 0;
1042
- let budgetUsedTokens = 0,
1043
- budgetMaxTokens = 0;
1044
- let successRuns = 0,
1045
- totalRuns = 0;
1046
-
1047
- // State: agent statuses
1048
- try {
1049
- const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
1050
- const agents = state.agents || {};
1051
- Object.values(agents).forEach((a) => {
1052
- if (a.status === 'running') agentsRunning++;
1053
- else agentsIdle++;
1054
- budgetUsedTokens += a.tokens_used || (a.tokens_in || 0) + (a.tokens_out || 0);
1055
- });
1056
- } catch (_) {}
1057
-
1058
- // Budget cap from org config
1059
- try {
1060
- const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
1061
- budgetMaxTokens = cfg.run_config?.budget_tokens || cfg.budget_tokens || 0;
1062
- } catch (_) {}
1063
-
1064
- // Issues: open count
1065
- try {
1066
- const iss = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
1067
- openIssues = (iss.issues || []).filter((i) => i.status === 'open').length;
1068
- inProgressIssues = (iss.issues || []).filter((i) => i.status === 'in_progress').length;
1069
- } catch (_) {}
1070
-
1071
- // Activity: 7-day success rate
1072
- try {
1073
- const actPath = path.join(base, `${orgName}-activity.jsonl`);
1074
- const lines = fs.readFileSync(actPath, 'utf8').split('\n').filter(Boolean);
1075
- const cutoffMs = Date.now() - 7 * 24 * 60 * 60 * 1000;
1076
- lines.forEach((line) => {
1077
- try {
1078
- const ev = JSON.parse(line);
1079
- const evMs = typeof ev.ts === 'number' ? ev.ts : ev.ts ? Date.parse(ev.ts) : 0;
1080
- if (!evMs || evMs < cutoffMs) return;
1081
- totalRuns++;
1082
- if (ev.type?.includes('complete')) successRuns++;
1083
- } catch (_) {}
1084
- });
1085
- } catch (_) {}
1086
-
1087
- const budgetUsedPct =
1088
- budgetMaxTokens > 0 ? Math.round((budgetUsedTokens / budgetMaxTokens) * 100) : null;
1089
- const successRate = totalRuns > 0 ? Math.round((successRuns / totalRuns) * 100) : null;
1090
-
1091
- res.writeHead(200, { 'Content-Type': 'application/json' });
1092
- res.end(
1093
- JSON.stringify({
1094
- agents_running: agentsRunning,
1095
- agents_idle: agentsIdle,
1096
- agents_active: agentsRunning,
1097
- open_issues: openIssues,
1098
- in_progress_issues: inProgressIssues,
1099
- tasks_pending: openIssues + inProgressIssues,
1100
- budget_used_tokens: budgetUsedTokens,
1101
- budget_max_tokens: budgetMaxTokens,
1102
- budget_used_pct: budgetUsedPct,
1103
- run_success_rate_7d: successRate,
1104
- total_runs_7d: totalRuns,
1105
- errors: [],
1106
- }),
1107
- );
1108
- } catch (_) {
1109
- res.writeHead(500);
1110
- res.end('{}');
1111
- }
1112
- return true;
1087
+ const orgName = decodeURIComponent(url.split('/')[3]);
1088
+ return hilRespond(res, corsOrigin, async () => {
1089
+ const v = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
1090
+ if (!v) throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
1091
+ const running = v.roles.filter((r) => r.status === 'running').length;
1092
+ const openTasks = v.tasks.filter((t) =>
1093
+ ['pending', 'ready', 'blocked'].includes(t.status),
1094
+ ).length;
1095
+ const activeTasks = v.tasks.filter((t) => t.status === 'running').length;
1096
+ const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
1097
+ const week = v.history.filter((h) => (h.endedAt || 0) >= weekAgo);
1098
+ // Only a boss's gate-checked org_complete with 'achieved' counts as success.
1099
+ const achieved = week.filter(
1100
+ (h) => h.closedBy === 'org-complete' && h.outcome?.status === 'achieved',
1101
+ ).length;
1102
+ const { used, tokens } = v.budget;
1103
+ return {
1104
+ agents_running: running,
1105
+ agents_idle: v.roles.length - running,
1106
+ agents_active: running,
1107
+ open_issues: openTasks,
1108
+ in_progress_issues: activeTasks,
1109
+ tasks_pending: openTasks + activeTasks,
1110
+ budget_used_tokens: used,
1111
+ budget_max_tokens: tokens,
1112
+ budget_basis: v.budget.basis,
1113
+ budget_used_pct: tokens && used !== null ? Math.round((used / tokens) * 100) : null,
1114
+ run_success_rate_7d: week.length ? Math.round((achieved / week.length) * 100) : null,
1115
+ total_runs_7d: week.length,
1116
+ errors: [...v.problems, ...(v.liveError ? [`daemon: ${v.liveError}`] : [])],
1117
+ };
1118
+ });
1113
1119
  }
1114
1120
 
1115
1121
  // GET /api/org/:name/environments — org execution environments (strips key material)
@@ -1362,6 +1368,38 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
1362
1368
  return true;
1363
1369
  }
1364
1370
 
1371
+ // GET /api/org/:name/runtime — what the org is doing now, from the runtime's
1372
+ // own sources (live daemon status, run bus, idle watchdog, history).
1373
+ if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runtime$/i)) {
1374
+ const orgName = decodeURIComponent(url.split('/')[3]);
1375
+ return hilRespond(res, corsOrigin, async () => {
1376
+ const view = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
1377
+ if (!view)
1378
+ throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
1379
+ return view;
1380
+ });
1381
+ }
1382
+
1383
+ // POST /api/org/:name/config — edit the org definition's goal, schedule,
1384
+ // run_config and default cost tier; refused unless the result passes the
1385
+ // same OrgDefSchema parse `org run` does.
1386
+ if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/config$/i)) {
1387
+ const orgName = decodeURIComponent(url.split('/')[3]);
1388
+ const body = await readHilBody(req);
1389
+ const root = hilRoot(req, ctx);
1390
+ return hilRespond(res, corsOrigin, () => {
1391
+ if (!body || typeof body !== 'object')
1392
+ throw Object.assign(new Error('JSON body required'), { status: 400 });
1393
+ try {
1394
+ return { ok: true, def: orgRuntime.patchOrgConfig(root, orgName, body) };
1395
+ } catch (err) {
1396
+ if (err instanceof orgRuntime.ConfigRejected)
1397
+ throw Object.assign(new Error(err.message), { status: 400 });
1398
+ throw err;
1399
+ }
1400
+ });
1401
+ }
1402
+
1365
1403
  // GET /api/org/:name/agents — agents from roles + merged heartbeat state
1366
1404
  if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agents$/i)) {
1367
1405
  try {
@@ -1390,17 +1428,25 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
1390
1428
  const roles = config.roles || [];
1391
1429
  const agents = roles.map((r) => {
1392
1430
  const s = agentState[r.id] || {};
1431
+ // Runtime and model the role actually runs with (cost tiers included).
1432
+ const m = orgRuntime.describeRoleModel(r, config);
1393
1433
  return {
1394
1434
  id: r.id,
1395
1435
  title: r.title || r.id,
1396
- adapterType: r.agent_type || r.type || null,
1397
- adapterModel: r.adapter_config?.model || r.adapter?.model || null,
1436
+ adapterType: m.runtime,
1437
+ adapterModel: m.model,
1438
+ effort: m.effort,
1439
+ tier: m.tier,
1398
1440
  governance: r.governance || null,
1399
1441
  reportsTo: r.reports_to || null,
1400
1442
  status: s.status || 'idle',
1401
1443
  lastHeartbeat: s.last_heartbeat || s.lastHeartbeat || null,
1402
1444
  tokensIn: s.tokens_in || 0,
1403
1445
  tokensOut: s.tokens_out || 0,
1446
+ cacheRead: s.cache_read_tokens || 0,
1447
+ cacheCreation: s.cache_creation_tokens || 0,
1448
+ tokensUsed: s.tokens_used || 0,
1449
+ costUsd: s.total_cost_usd || 0,
1404
1450
  };
1405
1451
  });
1406
1452
  res.writeHead(200, {
@@ -1415,409 +1461,124 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
1415
1461
  return true;
1416
1462
  }
1417
1463
 
1418
- // GET /api/org/:name/approvals — full approvals list with status filter support
1464
+ // GET /api/org/:name/approvals — the org's tool/action approvals
1465
+ // (<org>/approvals.json, written by orgrt/approvals.ts), newest first.
1419
1466
  if (
1420
1467
  req.method === 'GET' &&
1421
1468
  url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals(\?.*)?$/i)
1422
1469
  ) {
1423
- try {
1424
- const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1425
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1426
- res.writeHead(400);
1427
- res.end('Invalid org name');
1428
- return true;
1429
- }
1430
- const _approvalsQs = new URL(req.url, 'http://localhost').searchParams;
1431
- const base = path.join(
1432
- path.resolve(_approvalsQs.get('dir') || ctx.projectDir || process.cwd()),
1433
- '.monomind',
1434
- 'orgs',
1435
- );
1436
- const readJsonSafe = (f) => {
1437
- try {
1438
- return JSON.parse(fs.readFileSync(f, 'utf8'));
1439
- } catch (_) {
1440
- return null;
1441
- }
1442
- };
1443
- const data = readJsonSafe(path.join(base, `${orgName}-approvals.json`)) || { approvals: [] };
1444
- const approvals = (data.approvals || [])
1445
- .sort(
1446
- (a, b) =>
1447
- new Date(b.createdAt || b.created_at || b.requested_at || 0) -
1448
- new Date(a.createdAt || a.created_at || a.requested_at || 0),
1449
- )
1450
- .map((a) => ({
1451
- id: a.id,
1452
- title: a.title || a.action || null,
1453
- action: a.action || a.title || null,
1454
- description: a.description || a.action || a.title || null,
1455
- status: a.status || 'pending',
1456
- agentId: a.agentId || a.agent_id || null,
1457
- agentTitle: a.agentTitle || null,
1458
- requester: a.requester || a.agentTitle || a.agent_id || a.agentId || null,
1459
- agent: a.agent || a.agent_id || a.agentId || null,
1460
- payload: a.payload || null,
1461
- risk_level: a.risk_level || 'medium',
1462
- created_at: a.created_at || a.createdAt || a.requested_at || null,
1463
- createdAt: a.createdAt || a.created_at || a.requested_at || null,
1464
- updatedAt: a.updatedAt || null,
1465
- resolvedAt: a.resolvedAt || null,
1466
- resolvedBy: a.resolvedBy || null,
1467
- ts: a.ts || null,
1468
- }));
1469
- const pending = approvals.filter(
1470
- (a) => a.status === 'pending' || a.status === 'revision_requested',
1471
- ).length;
1472
- res.writeHead(200, {
1473
- 'Content-Type': 'application/json',
1474
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1475
- });
1476
- res.end(JSON.stringify({ approvals, pending }));
1477
- } catch (_) {
1478
- res.writeHead(500);
1479
- res.end('{"approvals":[],"pending":0}');
1480
- }
1481
- return true;
1470
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1471
+ return hilRespond(res, corsOrigin, () => {
1472
+ const approvals = hil.listApprovals(hilRoot(req, ctx), orgName);
1473
+ return { approvals, pending: approvals.filter((a) => a.status === 'pending').length };
1474
+ });
1482
1475
  }
1483
1476
 
1484
- // GET /api/org/:name/gates — decision gate log (read-only explorer; resolving stays
1485
- // CLI-only via `org gate-approve`/`gate-reject`, which needs the daemon's
1486
- // x-monomind-cred auth header for live delivery — out of scope for a read-only view)
1477
+ // GET /api/org/:name/gates — the org's decision gates (<org>/gates.json).
1487
1478
  if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates(\?.*)?$/i)) {
1488
- try {
1489
- const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1490
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1491
- res.writeHead(400);
1492
- res.end('Invalid org name');
1493
- return true;
1494
- }
1495
- const _gatesQs = new URL(req.url, 'http://localhost').searchParams;
1496
- const base = path.join(
1497
- path.resolve(_gatesQs.get('dir') || ctx.projectDir || process.cwd()),
1498
- '.monomind',
1499
- 'orgs',
1500
- );
1501
- const readJsonSafe = (f) => {
1502
- try {
1503
- return JSON.parse(fs.readFileSync(f, 'utf8'));
1504
- } catch (_) {
1505
- return null;
1506
- }
1507
- };
1508
- const data = readJsonSafe(path.join(base, orgName, 'gates.json')) || { gates: [] };
1509
- const gates = (data.gates || [])
1510
- .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
1511
- .map((g) => ({
1512
- id: g.id,
1513
- name: g.name,
1514
- description: g.description,
1515
- roleId: g.roleId,
1516
- status: g.status || 'pending',
1517
- createdAt: g.createdAt || null,
1518
- resolvedBy: g.resolvedBy || null,
1519
- resolvedAt: g.resolvedAt || null,
1520
- resolution: g.resolution || null,
1521
- }));
1522
- const pending = gates.filter((g) => g.status === 'pending').length;
1523
- res.writeHead(200, {
1524
- 'Content-Type': 'application/json',
1525
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1526
- });
1527
- res.end(JSON.stringify({ gates, pending }));
1528
- } catch (_) {
1529
- res.writeHead(500);
1530
- res.end('{"gates":[],"pending":0}');
1531
- }
1532
- return true;
1479
+ const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
1480
+ return hilRespond(res, corsOrigin, () => {
1481
+ const gates = hil.listGates(hilRoot(req, ctx), orgName);
1482
+ return { gates, pending: gates.filter((g) => g.status === 'pending').length };
1483
+ });
1533
1484
  }
1534
1485
 
1535
- // GET /api/questions?dir=<ctx.projectDir> — list ask_human questions (pending and
1536
- // answered) for every org in one project. Mirrors the existing -approvals.json
1537
- // sidecar convention, but reads this feature's .monomind/orgs/<org>/questions.json
1538
- // files (one per org dir). Each entry's `answer`/`answeredAt` fields (present once
1539
- // answered, absent while pending) let the dashboard split them into tabs.
1540
- if (req.method === 'GET' && url === '/api/questions') {
1541
- try {
1542
- const _qDir =
1543
- new URL(req.url, 'http://localhost').searchParams.get('dir') ||
1544
- ctx.projectDir ||
1545
- process.cwd();
1546
- const base = path.join(path.resolve(_qDir), '.monomind', 'orgs');
1547
- const out = [];
1548
- if (fs.existsSync(base)) {
1549
- for (const orgName of fs.readdirSync(base)) {
1550
- const qFile = path.join(base, orgName, 'questions.json');
1551
- if (!fs.existsSync(qFile)) continue;
1552
- let data = { questions: [] };
1553
- try {
1554
- data = JSON.parse(fs.readFileSync(qFile, 'utf8'));
1555
- } catch (_) {}
1556
- for (const q of data.questions || []) {
1557
- out.push({ org: orgName, ...q });
1558
- }
1559
- }
1560
- }
1561
- res.writeHead(200, {
1562
- 'Content-Type': 'application/json',
1563
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1564
- });
1565
- res.end(JSON.stringify({ questions: out }));
1566
- } catch (_e) {
1567
- res.writeHead(500);
1568
- res.end('{"questions":[]}');
1569
- }
1570
- return true;
1486
+ // GET /api/questions?dir= — ask_human questions (pending and answered) for
1487
+ // every org in one project, each with its `blocking` flag.
1488
+ if (req.method === 'GET' && (url === '/api/questions' || url.startsWith('/api/questions?'))) {
1489
+ return hilRespond(res, corsOrigin, () => ({
1490
+ questions: hil.pendingForProject(hilRoot(req, ctx)).questions,
1491
+ }));
1492
+ }
1493
+
1494
+ // GET /api/hil?dir= — everything in one project that waits on a human:
1495
+ // questions, approvals and decision gates across all its orgs.
1496
+ if (req.method === 'GET' && (url === '/api/hil' || url.startsWith('/api/hil?'))) {
1497
+ return hilRespond(res, corsOrigin, () => hil.pendingForProject(hilRoot(req, ctx)));
1571
1498
  }
1572
1499
 
1573
- // POST /api/questions/answer — forward a human's answer to the org's live process
1574
- // (looked up via the same file-based broker registry orgrt's cross-process delivery
1575
- // already uses), or fail with a clear error if no process anywhere hosts it.
1500
+ // POST /api/questions/answer { dir, org, questionId, answer } — live to the
1501
+ // hosting daemon (operator credential), else queued for the org's next run.
1576
1502
  if (req.method === 'POST' && url === '/api/questions/answer') {
1577
- let body = '';
1578
- for await (const chunk of req) {
1579
- body += chunk;
1580
- if (body.length > 2097152) {
1581
- req.destroy();
1582
- break;
1583
- }
1584
- }
1585
- try {
1586
- const parsed = JSON.parse(body);
1587
- const { dir, org, role, questionId, answer } = parsed;
1588
- if (!org || !role || !questionId || answer === undefined) {
1589
- res.writeHead(400, { 'Content-Type': 'application/json' });
1590
- res.end(JSON.stringify({ ok: false, error: 'org, role, questionId, answer are required' }));
1591
- return true;
1592
- }
1593
- if (String(org).length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(String(org))) {
1594
- res.writeHead(400, { 'Content-Type': 'application/json' });
1595
- res.end(JSON.stringify({ ok: false, error: 'Invalid org name' }));
1596
- return true;
1597
- }
1598
- const brokerDir = path.join(os.homedir(), '.monomind', 'orgrt-broker');
1599
- const entryPath = path.join(brokerDir, `${org}.json`);
1600
- let hostUrl = null;
1601
- try {
1602
- const entry = JSON.parse(fs.readFileSync(entryPath, 'utf8'));
1603
- if (Date.now() - entry.updatedAt < 90000) hostUrl = entry.url;
1604
- } catch (_) {}
1605
- if (!hostUrl) {
1606
- // No live process to forward to — the control server has no daemon instance of
1607
- // its own to call autoWake() on. If the org's definition still exists on disk,
1608
- // queue the answer the same way inbox.ts's queueMessage()/drainInbox() already
1609
- // do for offline cross-org messages, so it's delivered whenever the org next
1610
- // starts (manually or via its own schedule) — matching the offline-delivery goal
1611
- // for the dashboard path, not just the direct-daemon path Task 4 already covers.
1612
- const projDir = path.resolve(dir || ctx.projectDir || process.cwd());
1613
- const orgDefFile = path.join(projDir, '.monomind', 'orgs', `${org}.json`);
1614
- if (!fs.existsSync(orgDefFile)) {
1615
- res.writeHead(404, { 'Content-Type': 'application/json' });
1616
- res.end(
1617
- JSON.stringify({
1618
- ok: false,
1619
- error: `org "${org}" not found — no running process and no saved definition`,
1620
- }),
1621
- );
1622
- return true;
1623
- }
1624
- const qFile = path.join(projDir, '.monomind', 'orgs', org, 'questions.json');
1625
- let qData = { questions: [] };
1626
- try {
1627
- qData = JSON.parse(fs.readFileSync(qFile, 'utf8'));
1628
- } catch (_) {}
1629
- const qIdx = (qData.questions || []).findIndex((q) => q.questionId === questionId);
1630
- if (qIdx === -1) {
1631
- res.writeHead(404, { 'Content-Type': 'application/json' });
1632
- res.end(
1633
- JSON.stringify({
1634
- ok: false,
1635
- error: `question "${questionId}" not found for org "${org}"`,
1636
- }),
1637
- );
1638
- return true;
1639
- }
1640
- if (qData.questions[qIdx].answer !== null && qData.questions[qIdx].answer !== undefined) {
1641
- res.writeHead(200, { 'Content-Type': 'application/json' });
1642
- res.end(JSON.stringify({ ok: true, alreadyAnswered: true }));
1643
- return true;
1644
- }
1645
- const answeredQuestion = qData.questions[qIdx];
1646
- qData.questions[qIdx] = { ...answeredQuestion, answer, answeredAt: Date.now() };
1647
- const qTmp = `${qFile}.tmp`;
1648
- fs.writeFileSync(qTmp, JSON.stringify(qData, null, 2));
1649
- fs.renameSync(qTmp, qFile);
1650
- const inboxDir = path.join(projDir, '.monomind', 'orgs', org);
1651
- fs.mkdirSync(inboxDir, { recursive: true });
1652
- fs.appendFileSync(
1653
- path.join(inboxDir, 'inbox.jsonl'),
1654
- `${JSON.stringify({
1655
- fromQualified: 'human',
1656
- toRole: role,
1657
- subject: `answer:${questionId}`,
1658
- body: `question: ${answeredQuestion.question}\n\nanswer: ${answer}`,
1659
- ts: Date.now(),
1660
- })}\n`,
1661
- );
1662
- const queuedEvent = {
1663
- type: 'org:question-answered',
1664
- org,
1665
- role,
1666
- questionId,
1667
- ts: Date.now(),
1668
- queued: true,
1669
- };
1670
- ctx
1671
- .appendToFile(
1672
- path.join(projDir, 'data', 'mastermind-events.jsonl'),
1673
- `${JSON.stringify(queuedEvent)}\n`,
1674
- )
1675
- .catch(() => {});
1676
- ctx.broadcastMm(queuedEvent);
1677
- // Auto-wake: a queued answer is worthless if nothing ever restarts the org to
1678
- // drain it (unlike the direct-daemon path, this control server holds no OrgDaemon
1679
- // instance to call autoWake() on). `org run` itself no-ops if the org's runtime.json
1680
- // already shows a live pid, so this is safe to fire even if another process is
1681
- // racing to start it.
1682
- try {
1683
- const child = spawn('npx', ['-y', 'monomind@latest', 'org', 'run', org], {
1684
- cwd: projDir,
1685
- detached: true,
1686
- stdio: 'ignore',
1687
- });
1688
- child.unref();
1689
- } catch (_) {
1690
- /* best-effort — the answer is still queued on disk either way */
1691
- }
1692
- res.writeHead(200, { 'Content-Type': 'application/json' });
1693
- res.end(JSON.stringify({ ok: true, queued: true, waking: true }));
1694
- return true;
1695
- }
1696
- const fwd = await fetch(`${hostUrl}/api/answer-question`, {
1697
- method: 'POST',
1698
- headers: { 'Content-Type': 'application/json' },
1699
- body: JSON.stringify({ org, role, questionId, answer }),
1700
- signal: AbortSignal.timeout(5000),
1503
+ const body = await readHilBody(req);
1504
+ const { org, questionId, answer } = body || {};
1505
+ if (!validOrgName(org) || !questionId || typeof answer !== 'string' || !answer.trim())
1506
+ return hilRespond(res, corsOrigin, () => {
1507
+ throw Object.assign(new Error('org, questionId and a non-empty answer are required'), {
1508
+ status: 400,
1509
+ });
1701
1510
  });
1702
- const fwdData = await fwd.json().catch(() => ({}));
1703
- if (fwd.ok && fwdData.ok) {
1704
- const event = { type: 'org:question-answered', org, role, questionId, ts: Date.now() };
1705
- ctx
1706
- .appendToFile(
1707
- path.join(
1708
- path.resolve(dir || ctx.projectDir || process.cwd()),
1709
- 'data',
1710
- 'mastermind-events.jsonl',
1711
- ),
1712
- `${JSON.stringify(event)}\n`,
1713
- )
1714
- .catch(() => {});
1715
- ctx.broadcastMm(event);
1716
- res.writeHead(200, { 'Content-Type': 'application/json' });
1717
- res.end(JSON.stringify({ ok: true }));
1718
- } else {
1719
- res.writeHead(fwd.status || 500, { 'Content-Type': 'application/json' });
1720
- res.end(JSON.stringify({ ok: false, error: fwdData.error || 'answer delivery failed' }));
1721
- }
1722
- } catch (_e) {
1723
- res.writeHead(500);
1724
- res.end('{"ok":false}');
1725
- }
1726
- return true;
1511
+ const root = path.resolve(body.dir || ctx.projectDir || process.cwd());
1512
+ return hilRespond(res, corsOrigin, async () => {
1513
+ const r = await hil.answerQuestion(root, org, String(questionId), answer.trim());
1514
+ hilEvent(ctx, root, {
1515
+ type: 'org:question-answered',
1516
+ org,
1517
+ role: r.role,
1518
+ questionId,
1519
+ delivery: r.delivery,
1520
+ });
1521
+ return { ok: true, ...r };
1522
+ });
1727
1523
  }
1728
1524
 
1729
- // POST /api/org/:name/approvals/:id — approve or reject a pending approval request
1730
- // Body: { action: "approve" | "reject" | "revision_requested" }
1525
+ // POST /api/org/:name/gates/:id { approved: boolean, resolution? } — resolve
1526
+ // a decision gate, live or (org not running) in gates.json.
1731
1527
  if (
1732
1528
  req.method === 'POST' &&
1733
- url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/]+$/i)
1529
+ url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates\/[^/?]+(\?.*)?$/i)
1734
1530
  ) {
1735
- let body = '';
1736
- for await (const chunk of req) {
1737
- body += chunk;
1738
- if (body.length > 2097152) {
1739
- req.destroy();
1740
- break;
1741
- }
1742
- }
1743
- try {
1744
- const parts = url.split('/');
1745
- const orgName = decodeURIComponent(parts[3]);
1746
- const approvalId = decodeURIComponent(parts[5]);
1747
- if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
1748
- res.writeHead(400);
1749
- res.end('Invalid org name');
1750
- return true;
1751
- }
1752
- if (!approvalId) {
1753
- res.writeHead(400);
1754
- res.end('{"error":"approval id required"}');
1755
- return true;
1756
- }
1757
- const parsed = JSON.parse(body);
1758
- const action = parsed.action;
1759
- if (!['approve', 'reject', 'revision_requested'].includes(action)) {
1760
- res.writeHead(400);
1761
- res.end('{"error":"action must be approve, reject, or revision_requested"}');
1762
- return true;
1763
- }
1764
- const _postApprovalsQs = new URL(req.url, 'http://localhost').searchParams;
1765
- const base = path.join(
1766
- path.resolve(_postApprovalsQs.get('dir') || ctx.projectDir || process.cwd()),
1767
- '.monomind',
1768
- 'orgs',
1769
- );
1770
- const approvalsFile = path.join(base, `${orgName}-approvals.json`);
1771
- let data = { approvals: [] };
1772
- try {
1773
- data = JSON.parse(fs.readFileSync(approvalsFile, 'utf8'));
1774
- } catch (_) {}
1775
- const idx = (data.approvals || []).findIndex((a) => a.id === approvalId);
1776
- if (idx === -1) {
1777
- res.writeHead(404);
1778
- res.end('{"error":"approval not found"}');
1779
- return true;
1780
- }
1781
- const status =
1782
- action === 'approve' ? 'approved' : action === 'reject' ? 'rejected' : 'revision_requested';
1783
- data.approvals[idx] = {
1784
- ...data.approvals[idx],
1531
+ const parts = url.split('?')[0].split('/');
1532
+ const orgName = decodeURIComponent(parts[3]);
1533
+ const gateId = decodeURIComponent(parts[5]);
1534
+ const body = await readHilBody(req);
1535
+ const resolution =
1536
+ typeof body?.resolution === 'string' && body.resolution.trim()
1537
+ ? body.resolution.trim().slice(0, 4000)
1538
+ : undefined;
1539
+ const root = hilRoot(req, ctx);
1540
+ return hilRespond(res, corsOrigin, async () => {
1541
+ if (typeof body?.approved !== 'boolean')
1542
+ throw Object.assign(new Error('approved (boolean) is required'), { status: 400 });
1543
+ const r = await hil.resolveGate(root, orgName, gateId, body.approved, resolution);
1544
+ const status = body.approved ? 'approved' : 'rejected';
1545
+ hilEvent(ctx, root, {
1546
+ type: 'org:gate:resolved',
1547
+ org: orgName,
1548
+ gateId,
1785
1549
  status,
1786
- resolvedAt: new Date().toISOString(),
1787
- resolvedBy: 'operator',
1788
- };
1789
- const tmp = `${approvalsFile}.tmp`;
1790
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
1791
- fs.renameSync(tmp, approvalsFile);
1792
- // Emit org:approval:resolved event so boss agent unblocks
1793
- const event = {
1550
+ delivery: r.delivery,
1551
+ });
1552
+ return { ok: true, status, ...r };
1553
+ });
1554
+ }
1555
+
1556
+ // POST /api/org/:name/approvals/:requestId { action: "approve" | "reject" } —
1557
+ // resolve one tool/action approval, live or (org not running) in approvals.json.
1558
+ if (
1559
+ req.method === 'POST' &&
1560
+ url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/?]+(\?.*)?$/i)
1561
+ ) {
1562
+ const parts = url.split('?')[0].split('/');
1563
+ const orgName = decodeURIComponent(parts[3]);
1564
+ const requestId = decodeURIComponent(parts[5]);
1565
+ const body = await readHilBody(req);
1566
+ const root = hilRoot(req, ctx);
1567
+ return hilRespond(res, corsOrigin, async () => {
1568
+ if (body?.action !== 'approve' && body?.action !== 'reject')
1569
+ throw Object.assign(new Error('action must be approve or reject'), { status: 400 });
1570
+ const approved = body.action === 'approve';
1571
+ const r = await hil.resolveApproval(root, orgName, requestId, approved);
1572
+ const status = approved ? 'approved' : 'denied';
1573
+ hilEvent(ctx, root, {
1794
1574
  type: 'org:approval:resolved',
1795
1575
  org: orgName,
1796
- approval_id: approvalId,
1576
+ requestId,
1797
1577
  status,
1798
- ts: Date.now(),
1799
- };
1800
- ctx
1801
- .appendToFile(
1802
- path.join(
1803
- path.resolve(_postApprovalsQs.get('dir') || ctx.projectDir || process.cwd()),
1804
- 'data',
1805
- 'mastermind-events.jsonl',
1806
- ),
1807
- `${JSON.stringify(event)}\n`,
1808
- )
1809
- .catch(() => {});
1810
- ctx.broadcastMm(event);
1811
- res.writeHead(200, {
1812
- 'Content-Type': 'application/json',
1813
- ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1578
+ delivery: r.delivery,
1814
1579
  });
1815
- res.end(JSON.stringify({ ok: true, status }));
1816
- } catch (_) {
1817
- res.writeHead(500);
1818
- res.end('{}');
1819
- }
1820
- return true;
1580
+ return { ok: true, status, ...r };
1581
+ });
1821
1582
  }
1822
1583
 
1823
1584
  // GET /api/org/:name/secrets — masked secrets list (NEVER exposes values)
@@ -4403,6 +4164,41 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
4403
4164
  const _chMonoDir = ctx._getGitMonomindDir(_chRoot) || path.join(_chRoot, '.monomind');
4404
4165
  const _chRunId =
4405
4166
  ctx.activeOrgRuns.get(_chOrgName) || ctx._getActiveRunId(_chOrgName, _chRoot);
4167
+ // Deliver to the target role (default: the root role) — live into its
4168
+ // mailbox, or queued in the org's inbox for its next run.
4169
+ let _chRoles = [];
4170
+ try {
4171
+ const _chCfg = JSON.parse(
4172
+ fs.readFileSync(path.join(_chRoot, '.monomind', 'orgs', `${_chOrgName}.json`), 'utf8'),
4173
+ );
4174
+ if (Array.isArray(_chCfg.roles)) _chRoles = _chCfg.roles;
4175
+ } catch (_) {}
4176
+ const _chTargetRole =
4177
+ typeof _chPayload.role === 'string' && _chPayload.role
4178
+ ? _chRoles.find((r) => r.id === _chPayload.role)?.id
4179
+ : (_chRoles.find((r) => !r.reports_to) || _chRoles[0])?.id;
4180
+ if (!_chTargetRole) {
4181
+ res.writeHead(404, { 'Content-Type': 'application/json' });
4182
+ res.end(
4183
+ JSON.stringify({
4184
+ ok: false,
4185
+ error: _chPayload.role
4186
+ ? `org "${_chOrgName}" has no role "${_chPayload.role}"`
4187
+ : `org "${_chOrgName}" has no role to receive it`,
4188
+ }),
4189
+ );
4190
+ return true;
4191
+ }
4192
+ let _chDelivery;
4193
+ try {
4194
+ _chDelivery = (await hil.sendHumanMessage(_chRoot, _chOrgName, _chTargetRole, _chText))
4195
+ .delivery;
4196
+ } catch (err) {
4197
+ const { status, error } = hil.hilErrorStatus(err);
4198
+ res.writeHead(status, { 'Content-Type': 'application/json' });
4199
+ res.end(JSON.stringify({ ok: false, error }));
4200
+ return true;
4201
+ }
4406
4202
  const _chEvent = {
4407
4203
  type: 'user:message',
4408
4204
  org: _chOrgName,
@@ -4416,20 +4212,6 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
4416
4212
  if (fs.existsSync(_chRunFile))
4417
4213
  await ctx.appendToFile(_chRunFile, `${JSON.stringify(_chEvent)}\n`);
4418
4214
  }
4419
- // Write to mailbox file so boss agent can pick up on next cycle (durable
4420
- // record + offline fallback — the live-delivery attempt below is best-effort)
4421
- const _chMailbox = path.join(_chRoot, '.monomind', 'orgs', `${_chOrgName}-threads.json`);
4422
- try {
4423
- let _chThreads = { messages: [] };
4424
- if (fs.existsSync(_chMailbox)) {
4425
- try {
4426
- _chThreads = JSON.parse(fs.readFileSync(_chMailbox, 'utf8'));
4427
- } catch (_) {}
4428
- }
4429
- if (!Array.isArray(_chThreads.messages)) _chThreads.messages = [];
4430
- _chThreads.messages.push({ text: _chText, ts: _chEvent.ts, status: 'pending' });
4431
- fs.writeFileSync(_chMailbox, JSON.stringify(_chThreads, null, 2));
4432
- } catch (_) {}
4433
4215
  // Broadcast to SSE stream clients
4434
4216
  ctx.broadcastMm(_chEvent);
4435
4217
  const _chFwdClients = ctx.runStreamClients.get(_chOrgName);
@@ -4443,51 +4225,18 @@ export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
4443
4225
  }
4444
4226
  }
4445
4227
  }
4446
- // Forward to the org's live process (if any) so the message actually lands in a
4447
- // running role's mailbox — looked up via the same file-based broker registry
4448
- // orgrt's cross-process delivery already uses (mirrors POST /api/questions/answer).
4449
- let _chDelivered = false;
4450
- try {
4451
- const _chBrokerEntryPath = path.join(
4452
- os.homedir(),
4453
- '.monomind',
4454
- 'orgrt-broker',
4455
- `${_chOrgName}.json`,
4456
- );
4457
- const _chBrokerEntry = JSON.parse(fs.readFileSync(_chBrokerEntryPath, 'utf8'));
4458
- if (Date.now() - _chBrokerEntry.updatedAt < 90000 && _chBrokerEntry.url) {
4459
- let _chTargetRole = _chPayload.role;
4460
- if (!_chTargetRole) {
4461
- try {
4462
- const _chCfg = JSON.parse(
4463
- fs.readFileSync(
4464
- path.join(_chRoot, '.monomind', 'orgs', `${_chOrgName}.json`),
4465
- 'utf8',
4466
- ),
4467
- );
4468
- const _chRoles = Array.isArray(_chCfg.roles) ? _chCfg.roles : [];
4469
- const _chBoss =
4470
- _chRoles.find((r) => r.type === 'boss' || r.reports_to === null) || _chRoles[0];
4471
- _chTargetRole = _chBoss?.id;
4472
- } catch (_) {}
4473
- }
4474
- if (_chTargetRole) {
4475
- const _chFwd = await fetch(`${_chBrokerEntry.url}/api/human-message`, {
4476
- method: 'POST',
4477
- headers: { 'Content-Type': 'application/json' },
4478
- body: JSON.stringify({ org: _chOrgName, role: _chTargetRole, text: _chText }),
4479
- signal: AbortSignal.timeout(5000),
4480
- });
4481
- const _chFwdData = await _chFwd.json().catch(() => ({}));
4482
- _chDelivered = !!(_chFwd.ok && _chFwdData.ok);
4483
- }
4484
- }
4485
- } catch (_) {}
4486
4228
  res.writeHead(200, {
4487
4229
  'Content-Type': 'application/json',
4488
4230
  ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
4489
4231
  });
4490
- res.end(JSON.stringify({ ok: true, delivered: _chDelivered }));
4232
+ res.end(
4233
+ JSON.stringify({
4234
+ ok: true,
4235
+ delivered: _chDelivery === 'live',
4236
+ delivery: _chDelivery,
4237
+ role: _chTargetRole,
4238
+ }),
4239
+ );
4491
4240
  } catch (e) {
4492
4241
  res.writeHead(500, { 'Content-Type': 'application/json' });
4493
4242
  res.end(JSON.stringify({ error: e.message }));