@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.
- package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
- package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
- package/dist/src/commands/doctor-mcp-probe.js +205 -0
- package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +12 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +55 -4
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +10 -1
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +17 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +4 -3
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +14 -0
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +12 -2
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +2 -1
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/mcp-generator.d.ts.map +1 -1
- package/dist/src/init/mcp-generator.js +3 -3
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +6 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/init/write-runtime-config.d.ts.map +1 -1
- package/dist/src/init/write-runtime-config.js +12 -2
- package/dist/src/init/write-runtime-config.js.map +1 -1
- package/dist/src/orgrt/agent-runner.d.ts +38 -0
- package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
- package/dist/src/orgrt/agent-runner.js +47 -0
- package/dist/src/orgrt/agent-runner.js.map +1 -1
- package/dist/src/orgrt/checkpoint.d.ts +12 -1
- package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
- package/dist/src/orgrt/checkpoint.js +15 -6
- package/dist/src/orgrt/checkpoint.js.map +1 -1
- package/dist/src/orgrt/completion-gate.d.ts +74 -0
- package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
- package/dist/src/orgrt/completion-gate.js +45 -0
- package/dist/src/orgrt/completion-gate.js.map +1 -1
- package/dist/src/orgrt/cost-tier.d.ts +144 -0
- package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
- package/dist/src/orgrt/cost-tier.js +183 -0
- package/dist/src/orgrt/cost-tier.js.map +1 -0
- package/dist/src/orgrt/cross-org.d.ts.map +1 -1
- package/dist/src/orgrt/cross-org.js +14 -0
- package/dist/src/orgrt/cross-org.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +12 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +216 -49
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/decisions.d.ts +14 -2
- package/dist/src/orgrt/decisions.d.ts.map +1 -1
- package/dist/src/orgrt/decisions.js +216 -10
- package/dist/src/orgrt/decisions.js.map +1 -1
- package/dist/src/orgrt/file-roots.d.ts +7 -0
- package/dist/src/orgrt/file-roots.d.ts.map +1 -1
- package/dist/src/orgrt/file-roots.js +38 -1
- package/dist/src/orgrt/file-roots.js.map +1 -1
- package/dist/src/orgrt/forwarder.d.ts.map +1 -1
- package/dist/src/orgrt/forwarder.js +16 -2
- package/dist/src/orgrt/forwarder.js.map +1 -1
- package/dist/src/orgrt/idle-deadline.d.ts +74 -1
- package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
- package/dist/src/orgrt/idle-deadline.js +52 -2
- package/dist/src/orgrt/idle-deadline.js.map +1 -1
- package/dist/src/orgrt/loadouts.d.ts +38 -0
- package/dist/src/orgrt/loadouts.d.ts.map +1 -0
- package/dist/src/orgrt/loadouts.js +129 -0
- package/dist/src/orgrt/loadouts.js.map +1 -0
- package/dist/src/orgrt/mailbox.d.ts +17 -1
- package/dist/src/orgrt/mailbox.d.ts.map +1 -1
- package/dist/src/orgrt/mailbox.js +50 -3
- package/dist/src/orgrt/mailbox.js.map +1 -1
- package/dist/src/orgrt/policy.d.ts +42 -2
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +53 -8
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/prompt-vars.d.ts +11 -0
- package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
- package/dist/src/orgrt/prompt-vars.js +49 -0
- package/dist/src/orgrt/prompt-vars.js.map +1 -0
- package/dist/src/orgrt/questions.d.ts +22 -1
- package/dist/src/orgrt/questions.d.ts.map +1 -1
- package/dist/src/orgrt/questions.js +24 -3
- package/dist/src/orgrt/questions.js.map +1 -1
- package/dist/src/orgrt/review-packet.d.ts +29 -0
- package/dist/src/orgrt/review-packet.d.ts.map +1 -0
- package/dist/src/orgrt/review-packet.js +67 -0
- package/dist/src/orgrt/review-packet.js.map +1 -0
- package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
- package/dist/src/orgrt/role-sandbox.js +16 -3
- package/dist/src/orgrt/role-sandbox.js.map +1 -1
- package/dist/src/orgrt/session-ledger.d.ts +61 -0
- package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
- package/dist/src/orgrt/session-ledger.js +113 -0
- package/dist/src/orgrt/session-ledger.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +45 -5
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +384 -27
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/task-dag.d.ts +34 -1
- package/dist/src/orgrt/task-dag.d.ts.map +1 -1
- package/dist/src/orgrt/task-dag.js +58 -7
- package/dist/src/orgrt/task-dag.js.map +1 -1
- package/dist/src/orgrt/tool-spill.d.ts +94 -0
- package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
- package/dist/src/orgrt/tool-spill.js +180 -0
- package/dist/src/orgrt/tool-spill.js.map +1 -0
- package/dist/src/orgrt/types.d.ts +138 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +148 -0
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
- package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.js +25 -5
- package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
- package/dist/src/protocol-capabilities.d.ts +2 -1
- package/dist/src/protocol-capabilities.d.ts.map +1 -1
- package/dist/src/protocol-capabilities.js +2 -1
- package/dist/src/protocol-capabilities.js.map +1 -1
- package/dist/src/ui/dashboard.html +450 -164
- package/dist/src/ui/org-hil.mjs +276 -0
- package/dist/src/ui/org-runtime.mjs +350 -0
- package/dist/src/ui/orgs.html +15 -4
- package/dist/src/ui/routes-org.mjs +290 -541
- package/dist/src/ui/server.mjs +25 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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 =
|
|
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
|
-
|
|
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.
|
|
442
|
-
msg:
|
|
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
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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:
|
|
1397
|
-
adapterModel:
|
|
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 —
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
|
1574
|
-
//
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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/
|
|
1730
|
-
//
|
|
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}\/
|
|
1529
|
+
url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates\/[^/?]+(\?.*)?$/i)
|
|
1734
1530
|
) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
1788
|
-
};
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
1576
|
+
requestId,
|
|
1797
1577
|
status,
|
|
1798
|
-
|
|
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
|
-
|
|
1816
|
-
}
|
|
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(
|
|
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 }));
|