@monoes/monomindcli 2.14.1 → 2.15.1

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 (197) hide show
  1. package/.claude/helpers/control-start.cjs +10 -13
  2. package/.claude/skills/monodesign/scripts/context.mjs +4 -4
  3. package/.claude/skills/monodesign/scripts/critique-storage.mjs +1 -1
  4. package/.claude/skills/monodesign/scripts/detect-csp.mjs +1 -6
  5. package/.claude/skills/monodesign/scripts/detector/browser/injected/index.mjs +16 -18
  6. package/.claude/skills/monodesign/scripts/detector/cli/main.mjs +3 -3
  7. package/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +67 -47
  8. package/.claude/skills/monodesign/scripts/detector/engines/regex/detect-text.mjs +6 -6
  9. package/.claude/skills/monodesign/scripts/detector/engines/static-html/css-cascade.mjs +6 -5
  10. package/.claude/skills/monodesign/scripts/detector/engines/static-html/detect-html.mjs +6 -6
  11. package/.claude/skills/monodesign/scripts/detector/fix/index.mjs +3 -3
  12. package/.claude/skills/monodesign/scripts/detector/node/file-system.mjs +1 -1
  13. package/.claude/skills/monodesign/scripts/detector/registry/antipatterns.mjs +1 -1
  14. package/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +24 -25
  15. package/.claude/skills/monodesign/scripts/detector/shared/color.mjs +2 -2
  16. package/.claude/skills/monodesign/scripts/detector/shared/constants.mjs +1 -1
  17. package/.claude/skills/monodesign/scripts/detector/shared/inline-ignores.mjs +2 -2
  18. package/.claude/skills/monodesign/scripts/hook-admin.mjs +6 -6
  19. package/.claude/skills/monodesign/scripts/hook-before-edit.mjs +4 -4
  20. package/.claude/skills/monodesign/scripts/hook-lib.mjs +20 -11
  21. package/.claude/skills/monodesign/scripts/hook.mjs +1 -1
  22. package/.claude/skills/monodesign/scripts/lib/design-parser.mjs +9 -42
  23. package/.claude/skills/monodesign/scripts/lib/is-generated.mjs +1 -1
  24. package/.claude/skills/monodesign/scripts/lib/monodesign-config.mjs +5 -5
  25. package/.claude/skills/monodesign/scripts/lib/monodesign-paths.mjs +1 -1
  26. package/.claude/skills/monodesign/scripts/live/event-validation.mjs +13 -13
  27. package/.claude/skills/monodesign/scripts/live/manual-apply.mjs +4 -4
  28. package/.claude/skills/monodesign/scripts/live/manual-edits-buffer.mjs +1 -1
  29. package/.claude/skills/monodesign/scripts/live/session-store.mjs +5 -5
  30. package/.claude/skills/monodesign/scripts/live/svelte-component.mjs +13 -13
  31. package/.claude/skills/monodesign/scripts/live/sveltekit-adapter.mjs +4 -4
  32. package/.claude/skills/monodesign/scripts/live/ui-core.mjs +1 -1
  33. package/.claude/skills/monodesign/scripts/live-accept.mjs +24 -24
  34. package/.claude/skills/monodesign/scripts/live-browser-dom.js +8 -7
  35. package/.claude/skills/monodesign/scripts/live-browser-session.js +8 -7
  36. package/.claude/skills/monodesign/scripts/live-browser.js +405 -432
  37. package/.claude/skills/monodesign/scripts/live-commit-manual-edits.mjs +6 -6
  38. package/.claude/skills/monodesign/scripts/live-copy-edit-agent.mjs +2 -2
  39. package/.claude/skills/monodesign/scripts/live-discard-manual-edits.mjs +1 -1
  40. package/.claude/skills/monodesign/scripts/live-inject.mjs +7 -7
  41. package/.claude/skills/monodesign/scripts/live-insert.mjs +11 -11
  42. package/.claude/skills/monodesign/scripts/live-manual-edit-evidence.mjs +4 -4
  43. package/.claude/skills/monodesign/scripts/live-poll.mjs +5 -5
  44. package/.claude/skills/monodesign/scripts/live-server.mjs +10 -10
  45. package/.claude/skills/monodesign/scripts/live-wrap.mjs +31 -41
  46. package/.claude/skills/monodesign/scripts/live.mjs +5 -5
  47. package/.claude/skills/monodesign/scripts/palette.mjs +3 -1
  48. package/.claude/skills/monodesign/scripts/pin.mjs +1 -1
  49. package/dist/src/commands/doc-filters.d.ts.map +1 -1
  50. package/dist/src/commands/doc-filters.js +6 -0
  51. package/dist/src/commands/doc-filters.js.map +1 -1
  52. package/dist/src/commands/doc-library.d.ts +2 -1
  53. package/dist/src/commands/doc-library.d.ts.map +1 -1
  54. package/dist/src/commands/doc-library.js +66 -2
  55. package/dist/src/commands/doc-library.js.map +1 -1
  56. package/dist/src/commands/doc.d.ts.map +1 -1
  57. package/dist/src/commands/doc.js +2 -0
  58. package/dist/src/commands/doc.js.map +1 -1
  59. package/dist/src/commands/org.d.ts.map +1 -1
  60. package/dist/src/commands/org.js +42 -4
  61. package/dist/src/commands/org.js.map +1 -1
  62. package/dist/src/commands/ui.d.ts.map +1 -1
  63. package/dist/src/commands/ui.js +60 -0
  64. package/dist/src/commands/ui.js.map +1 -1
  65. package/dist/src/knowledge/capture-envelope.d.ts +5 -0
  66. package/dist/src/knowledge/capture-envelope.d.ts.map +1 -1
  67. package/dist/src/knowledge/capture-envelope.js +2 -0
  68. package/dist/src/knowledge/capture-envelope.js.map +1 -1
  69. package/dist/src/knowledge/document-chunking.d.ts +42 -0
  70. package/dist/src/knowledge/document-chunking.d.ts.map +1 -0
  71. package/dist/src/knowledge/document-chunking.js +288 -0
  72. package/dist/src/knowledge/document-chunking.js.map +1 -0
  73. package/dist/src/knowledge/document-index.d.ts +124 -0
  74. package/dist/src/knowledge/document-index.d.ts.map +1 -0
  75. package/dist/src/knowledge/document-index.js +314 -0
  76. package/dist/src/knowledge/document-index.js.map +1 -0
  77. package/dist/src/knowledge/document-ingest.d.ts +23 -0
  78. package/dist/src/knowledge/document-ingest.d.ts.map +1 -0
  79. package/dist/src/knowledge/document-ingest.js +368 -0
  80. package/dist/src/knowledge/document-ingest.js.map +1 -0
  81. package/dist/src/knowledge/document-pipeline.d.ts +26 -208
  82. package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
  83. package/dist/src/knowledge/document-pipeline.js +25 -1104
  84. package/dist/src/knowledge/document-pipeline.js.map +1 -1
  85. package/dist/src/knowledge/document-search.d.ts +27 -0
  86. package/dist/src/knowledge/document-search.d.ts.map +1 -0
  87. package/dist/src/knowledge/document-search.js +138 -0
  88. package/dist/src/knowledge/document-search.js.map +1 -0
  89. package/dist/src/knowledge/document-store.d.ts +28 -0
  90. package/dist/src/knowledge/document-store.d.ts.map +1 -0
  91. package/dist/src/knowledge/document-store.js +61 -0
  92. package/dist/src/knowledge/document-store.js.map +1 -0
  93. package/dist/src/knowledge/document-types.d.ts +103 -0
  94. package/dist/src/knowledge/document-types.d.ts.map +1 -0
  95. package/dist/src/knowledge/document-types.js +12 -0
  96. package/dist/src/knowledge/document-types.js.map +1 -0
  97. package/dist/src/knowledge/highlights.d.ts +168 -0
  98. package/dist/src/knowledge/highlights.d.ts.map +1 -0
  99. package/dist/src/knowledge/highlights.js +311 -0
  100. package/dist/src/knowledge/highlights.js.map +1 -0
  101. package/dist/src/knowledge/lookup.d.ts +80 -0
  102. package/dist/src/knowledge/lookup.d.ts.map +1 -0
  103. package/dist/src/knowledge/lookup.js +156 -0
  104. package/dist/src/knowledge/lookup.js.map +1 -0
  105. package/dist/src/knowledge/okf-bundle.d.ts +19 -0
  106. package/dist/src/knowledge/okf-bundle.d.ts.map +1 -0
  107. package/dist/src/knowledge/okf-bundle.js +106 -0
  108. package/dist/src/knowledge/okf-bundle.js.map +1 -0
  109. package/dist/src/knowledge/profile-store.d.ts +134 -0
  110. package/dist/src/knowledge/profile-store.d.ts.map +1 -0
  111. package/dist/src/knowledge/profile-store.js +237 -0
  112. package/dist/src/knowledge/profile-store.js.map +1 -0
  113. package/dist/src/orgrt/agent-runner.d.ts +4 -0
  114. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  115. package/dist/src/orgrt/agent-runner.js +22 -0
  116. package/dist/src/orgrt/agent-runner.js.map +1 -1
  117. package/dist/src/orgrt/antigravity-runner.d.ts.map +1 -1
  118. package/dist/src/orgrt/antigravity-runner.js +2 -1
  119. package/dist/src/orgrt/antigravity-runner.js.map +1 -1
  120. package/dist/src/orgrt/authority-mask.d.ts +32 -0
  121. package/dist/src/orgrt/authority-mask.d.ts.map +1 -0
  122. package/dist/src/orgrt/authority-mask.js +135 -0
  123. package/dist/src/orgrt/authority-mask.js.map +1 -0
  124. package/dist/src/orgrt/codex-runner.d.ts.map +1 -1
  125. package/dist/src/orgrt/codex-runner.js +2 -1
  126. package/dist/src/orgrt/codex-runner.js.map +1 -1
  127. package/dist/src/orgrt/copilot-runner.d.ts.map +1 -1
  128. package/dist/src/orgrt/copilot-runner.js +2 -1
  129. package/dist/src/orgrt/copilot-runner.js.map +1 -1
  130. package/dist/src/orgrt/crush-runner.d.ts.map +1 -1
  131. package/dist/src/orgrt/crush-runner.js +6 -1
  132. package/dist/src/orgrt/crush-runner.js.map +1 -1
  133. package/dist/src/orgrt/daemon.d.ts +4 -0
  134. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  135. package/dist/src/orgrt/daemon.js +13 -2
  136. package/dist/src/orgrt/daemon.js.map +1 -1
  137. package/dist/src/orgrt/decisions.d.ts +9 -0
  138. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  139. package/dist/src/orgrt/decisions.js +32 -5
  140. package/dist/src/orgrt/decisions.js.map +1 -1
  141. package/dist/src/orgrt/file-roots.d.ts +2 -1
  142. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  143. package/dist/src/orgrt/file-roots.js +7 -3
  144. package/dist/src/orgrt/file-roots.js.map +1 -1
  145. package/dist/src/orgrt/grok-runner.d.ts.map +1 -1
  146. package/dist/src/orgrt/grok-runner.js +2 -1
  147. package/dist/src/orgrt/grok-runner.js.map +1 -1
  148. package/dist/src/orgrt/hermes-runner.d.ts.map +1 -1
  149. package/dist/src/orgrt/hermes-runner.js +2 -1
  150. package/dist/src/orgrt/hermes-runner.js.map +1 -1
  151. package/dist/src/orgrt/inbox.d.ts.map +1 -1
  152. package/dist/src/orgrt/inbox.js +70 -4
  153. package/dist/src/orgrt/inbox.js.map +1 -1
  154. package/dist/src/orgrt/kimicode-runner.d.ts.map +1 -1
  155. package/dist/src/orgrt/kimicode-runner.js +2 -1
  156. package/dist/src/orgrt/kimicode-runner.js.map +1 -1
  157. package/dist/src/orgrt/opencode-runner.d.ts.map +1 -1
  158. package/dist/src/orgrt/opencode-runner.js +3 -2
  159. package/dist/src/orgrt/opencode-runner.js.map +1 -1
  160. package/dist/src/orgrt/pi-rpc-runner.d.ts.map +1 -1
  161. package/dist/src/orgrt/pi-rpc-runner.js +3 -2
  162. package/dist/src/orgrt/pi-rpc-runner.js.map +1 -1
  163. package/dist/src/orgrt/pi-runner.d.ts.map +1 -1
  164. package/dist/src/orgrt/pi-runner.js +2 -1
  165. package/dist/src/orgrt/pi-runner.js.map +1 -1
  166. package/dist/src/orgrt/policy.d.ts.map +1 -1
  167. package/dist/src/orgrt/policy.js +3 -0
  168. package/dist/src/orgrt/policy.js.map +1 -1
  169. package/dist/src/orgrt/qwen-rpc-runner.d.ts.map +1 -1
  170. package/dist/src/orgrt/qwen-rpc-runner.js +3 -2
  171. package/dist/src/orgrt/qwen-rpc-runner.js.map +1 -1
  172. package/dist/src/orgrt/qwen-runner.d.ts.map +1 -1
  173. package/dist/src/orgrt/qwen-runner.js +2 -1
  174. package/dist/src/orgrt/qwen-runner.js.map +1 -1
  175. package/dist/src/orgrt/role-sandbox.d.ts +21 -0
  176. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  177. package/dist/src/orgrt/role-sandbox.js +31 -6
  178. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  179. package/dist/src/orgrt/session.d.ts.map +1 -1
  180. package/dist/src/orgrt/session.js +21 -2
  181. package/dist/src/orgrt/session.js.map +1 -1
  182. package/dist/src/orgrt/task-dag.d.ts +3 -0
  183. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  184. package/dist/src/orgrt/task-dag.js +1 -0
  185. package/dist/src/orgrt/task-dag.js.map +1 -1
  186. package/dist/src/orgrt/types.d.ts +5 -0
  187. package/dist/src/orgrt/types.d.ts.map +1 -1
  188. package/dist/src/orgrt/types.js +23 -1
  189. package/dist/src/orgrt/types.js.map +1 -1
  190. package/dist/src/ui/dashboard.html +28 -848
  191. package/dist/src/ui/human-auth.mjs +103 -0
  192. package/dist/src/ui/org-hil.mjs +86 -37
  193. package/dist/src/ui/org-runtime.mjs +21 -13
  194. package/dist/src/ui/routes-org.mjs +49 -445
  195. package/dist/src/ui/server.mjs +103 -4
  196. package/dist/tsconfig.tsbuildinfo +1 -1
  197. package/package.json +6 -6
@@ -0,0 +1,103 @@
1
+ // Proof that a human is at the browser, for the dashboard.
2
+ //
3
+ // The dashboard token (<project>/.monomind/dashboard-token) is a machine
4
+ // credential: hooks, the CLI and the org forwarder send it. It must not be
5
+ // enough to act as the human who supervises an org — approve a tool call,
6
+ // resolve a gate, answer a question, message a role, change an org's config —
7
+ // because anything running as this user, org roles included, can reach the
8
+ // dashboard's port, and the page used to hand the token to any GET /.
9
+ //
10
+ // Human authority is instead a browser session cookie derived from a secret
11
+ // in ~/.monomind/dashboard-auth/, a directory every org role is masked from
12
+ // (orgrt/authority-mask.ts). A browser gets the cookie by opening a one-time
13
+ // login link — `monomind dashboard open`, or the tab the dashboard opens when
14
+ // it starts — whose nonce is a file in that same directory, used once and
15
+ // short-lived, so a copy left in browser history is worthless.
16
+ import crypto from 'node:crypto';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ export const HUMAN_COOKIE = 'mm_human';
22
+ const NONCE_TTL_MS = 10 * 60_000;
23
+ const SESSION_MAX_AGE_S = 30 * 24 * 60 * 60;
24
+
25
+ export function humanAuthDir(home = os.homedir()) {
26
+ return path.join(home, '.monomind', 'dashboard-auth');
27
+ }
28
+
29
+ /** The per-user secret, created owner-only on first use. */
30
+ function secret(home) {
31
+ const dir = humanAuthDir(home);
32
+ const file = path.join(dir, 'secret');
33
+ try {
34
+ return fs.readFileSync(file);
35
+ } catch {
36
+ /* create below */
37
+ }
38
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
39
+ try {
40
+ fs.writeFileSync(file, crypto.randomBytes(32), { mode: 0o600, flag: 'wx' });
41
+ } catch (err) {
42
+ if (err.code !== 'EEXIST') throw err; // another process created it first
43
+ }
44
+ return fs.readFileSync(file);
45
+ }
46
+
47
+ function sessionValue(home) {
48
+ return crypto.createHmac('sha256', secret(home)).update('dashboard-session-v1').digest('hex');
49
+ }
50
+
51
+ /** A new one-time login nonce (the `?login=` of a login link). */
52
+ export function issueLoginNonce(home = os.homedir()) {
53
+ secret(home); // the dir exists, owner-only
54
+ const pending = path.join(humanAuthDir(home), 'login');
55
+ fs.mkdirSync(pending, { recursive: true, mode: 0o700 });
56
+ const nonce = crypto.randomBytes(32).toString('hex');
57
+ fs.writeFileSync(path.join(pending, nonce), '', { mode: 0o600, flag: 'wx' });
58
+ return nonce;
59
+ }
60
+
61
+ /** Spend a login nonce: true once, for a nonce issued in the last 10 minutes. */
62
+ export function consumeLoginNonce(nonce, home = os.homedir()) {
63
+ if (typeof nonce !== 'string' || !/^[0-9a-f]{64}$/.test(nonce)) return false;
64
+ const file = path.join(humanAuthDir(home), 'login', nonce);
65
+ let st;
66
+ try {
67
+ st = fs.statSync(file);
68
+ fs.unlinkSync(file); // used once, whatever the outcome below
69
+ } catch {
70
+ return false;
71
+ }
72
+ return Date.now() - st.mtimeMs <= NONCE_TTL_MS;
73
+ }
74
+
75
+ /** Set-Cookie value that marks this browser as the human's. */
76
+ export function humanSessionCookie(home = os.homedir()) {
77
+ return `${HUMAN_COOKIE}=${sessionValue(home)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_MAX_AGE_S}`;
78
+ }
79
+
80
+ /** Whether `req` carries the human session cookie. */
81
+ export function isHumanRequest(req, home = os.homedir()) {
82
+ const header = req.headers?.cookie;
83
+ if (!header) return false;
84
+ const want = Buffer.from(sessionValue(home));
85
+ for (const part of String(header).split(';')) {
86
+ const [k, ...v] = part.trim().split('=');
87
+ if (k !== HUMAN_COOKIE) continue;
88
+ const got = Buffer.from(v.join('='));
89
+ if (got.length === want.length && crypto.timingSafeEqual(got, want)) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ /** Page for a browser without the session: how to get a login link. */
95
+ export function loginRequiredPage() {
96
+ return `<!doctype html><meta charset="utf-8"><title>monomind dashboard</title>
97
+ <style>body{font:15px/1.5 system-ui,sans-serif;background:#0b0b0c;color:#ddd;display:grid;place-items:center;min-height:100vh;margin:0}
98
+ main{max-width:34rem;padding:2rem}code{background:#222;padding:.15em .4em;border-radius:4px}</style>
99
+ <main><h1 style="font-size:1.2rem">Open the dashboard from your terminal</h1>
100
+ <p>This browser has no dashboard session. Run</p><p><code>monomind dashboard open</code></p>
101
+ <p>It opens a one-time login link. The session then lasts 30 days in this browser.</p>
102
+ <p style="color:#888;font-size:13px">The dashboard can approve and answer for you, so it only trusts a browser you logged in yourself — not every program that can reach this port.</p></main>`;
103
+ }
@@ -8,10 +8,11 @@
8
8
  // decision is recorded in the org's own files, where the next run finds it.
9
9
  // Unlike the CLI, a live daemon that REJECTS the call is reported as an error
10
10
  // instead of falling back to a file write the daemon would overwrite.
11
+ import { randomBytes } from 'node:crypto';
11
12
  import fs from 'node:fs';
12
13
  import path from 'node:path';
13
14
  // Compiled from orgrt/*.ts, like forwarder.js in routes-org.mjs.
14
- import { lookupOrg, readOperatorCredential } from '../orgrt/broker.js';
15
+ import { lookupOrg, normalizeRoot, readOperatorCredential } from '../orgrt/broker.js';
15
16
  import { queueMessage } from '../orgrt/inbox.js';
16
17
 
17
18
  /** Recorded as resolvedBy on everything decided here (normalizeResolver-valid). */
@@ -49,20 +50,48 @@ function readList(root, org, file, key) {
49
50
  return list;
50
51
  }
51
52
 
53
+ /** Write `text` to `dest` via a fresh temp file and a rename. Roles can write
54
+ * into the org directory, so the temp name is unpredictable and created
55
+ * exclusively (O_EXCL): a link planted there fails the open instead of
56
+ * redirecting the dashboard's write to a file of the link's choosing. */
57
+ export function writeFileAtomic(dest, text) {
58
+ const tmp = `${dest}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
59
+ const fd = fs.openSync(tmp, 'wx', 0o600);
60
+ try {
61
+ try {
62
+ fs.writeFileSync(fd, text);
63
+ } finally {
64
+ fs.closeSync(fd);
65
+ }
66
+ fs.renameSync(tmp, dest);
67
+ } catch (err) {
68
+ fs.rmSync(tmp, { force: true });
69
+ throw err;
70
+ }
71
+ }
72
+
52
73
  function writeList(root, org, file, key, list) {
53
74
  const dir = orgDir(root, org);
54
75
  fs.mkdirSync(dir, { recursive: true });
55
- const dest = path.join(dir, file);
56
- const tmp = `${dest}.${process.pid}.tmp`;
57
- fs.writeFileSync(tmp, JSON.stringify({ [key]: list }, null, 2));
58
- fs.renameSync(tmp, dest);
76
+ writeFileAtomic(path.join(dir, file), JSON.stringify({ [key]: list }, null, 2));
59
77
  }
60
78
 
61
- /** POST to the hosting daemon's operator-only `route`. Returns null when no
62
- * live daemon hosts `org`; otherwise the daemon's verdict. */
63
- async function callDaemon(org, route, body) {
79
+ /** The broker entry of the daemon hosting `org` for THIS project, or null.
80
+ * The registry is machine-wide and keyed by name, so a same-named org in
81
+ * another project must not receive this project's decisions. An entry that
82
+ * predates the root field (M4) is taken as this project's. */
83
+ export function hostingDaemon(root, org) {
64
84
  const remote = lookupOrg(org);
65
85
  if (!remote) return null;
86
+ if (remote.root && remote.root !== normalizeRoot(root)) return null;
87
+ return remote;
88
+ }
89
+
90
+ /** POST to the hosting daemon's operator-only `route`. Returns null when no
91
+ * live daemon hosts `org` for `root`; otherwise the daemon's verdict. */
92
+ async function callDaemon(root, org, route, body) {
93
+ const remote = hostingDaemon(root, org);
94
+ if (!remote) return null;
66
95
  const cred = readOperatorCredential(org);
67
96
  if (!cred)
68
97
  throw new HilError(
@@ -84,7 +113,7 @@ async function callDaemon(org, route, body) {
84
113
  if (!res.ok || !data.ok)
85
114
  throw new HilError(
86
115
  res.status >= 400 ? res.status : 502,
87
- data.error || `daemon returned ${res.status}`,
116
+ data.error || data.receipt || `daemon returned ${res.status}`,
88
117
  );
89
118
  return data;
90
119
  }
@@ -95,15 +124,19 @@ function approvalStatus(a) {
95
124
  return 'pending';
96
125
  }
97
126
 
98
- /** The org's tool/action approvals (`<org>/approvals.json`), newest first. */
127
+ /** The org's tool/action approvals (`<org>/approvals.json`), newest first.
128
+ * An approval request lives in the daemon's memory and ends with its run —
129
+ * no daemon reads approvals.json back — so while no daemon hosts the org a
130
+ * pending one is 'expired': nothing is waiting for the answer. */
99
131
  export function listApprovals(root, org) {
132
+ const live = !!hostingDaemon(root, org);
100
133
  return readList(root, org, 'approvals.json', 'approvals')
101
134
  .map((a) => ({
102
135
  id: a.requestId || null,
103
136
  roleId: a.roleId,
104
137
  action: a.action,
105
138
  input: a.input || null,
106
- status: approvalStatus(a),
139
+ status: approvalStatus(a) === 'pending' && !live ? 'expired' : approvalStatus(a),
107
140
  ts: a.ts || null,
108
141
  resolvedBy: a.resolvedBy || null,
109
142
  resolvedAt: a.resolvedAt || null,
@@ -150,12 +183,17 @@ export function pendingForProject(root) {
150
183
  for (const e of entries) {
151
184
  if (!e.isDirectory() || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(e.name)) continue;
152
185
  const org = e.name;
153
- try {
154
- for (const q of listQuestions(root, org)) out.questions.push({ org, ...q });
155
- for (const a of listApprovals(root, org)) out.approvals.push({ org, ...a });
156
- for (const g of listGates(root, org)) out.gates.push({ org, ...g });
157
- } catch (err) {
158
- out.errors.push({ org, error: err.message });
186
+ // One unreadable file must not hide the org's other two.
187
+ for (const [list, key] of [
188
+ [listQuestions, 'questions'],
189
+ [listApprovals, 'approvals'],
190
+ [listGates, 'gates'],
191
+ ]) {
192
+ try {
193
+ for (const item of list(root, org)) out[key].push({ org, ...item });
194
+ } catch (err) {
195
+ out.errors.push({ org, error: err.message });
196
+ }
159
197
  }
160
198
  }
161
199
  return out;
@@ -168,24 +206,35 @@ export async function resolveApproval(root, org, requestId, approved) {
168
206
  if (!entry) throw new HilError(404, `approval "${requestId}" not found for org "${org}"`);
169
207
  if (entry.approved !== null)
170
208
  throw new HilError(409, `approval "${requestId}" already ${approvalStatus(entry)}`);
171
- const live = await callDaemon(org, '/api/set-approval', {
172
- org,
173
- role: entry.roleId,
174
- action: entry.action,
175
- approved,
176
- resolvedBy: DASHBOARD_RESOLVER,
177
- requestId,
178
- });
209
+ const ended = `approval "${requestId}" ended with the run that asked for it; the role asks again when it needs to`;
210
+ let live;
211
+ try {
212
+ live = await callDaemon(root, org, '/api/set-approval', {
213
+ org,
214
+ role: entry.roleId,
215
+ action: entry.action,
216
+ approved,
217
+ resolvedBy: DASHBOARD_RESOLVER,
218
+ requestId,
219
+ });
220
+ } catch (err) {
221
+ // A daemon started since (e.g. `--resume` in a new process) holds only
222
+ // the approvals its own run asked for.
223
+ if (err instanceof HilError && err.status === 404) {
224
+ // …or someone resolved it first: the daemon writes the file before it answers.
225
+ const now = readList(root, org, 'approvals.json', 'approvals').find(
226
+ (a) => a.requestId === requestId,
227
+ );
228
+ if (now && now.approved !== null)
229
+ throw new HilError(409, `approval "${requestId}" already ${approvalStatus(now)}`);
230
+ throw new HilError(409, ended);
231
+ }
232
+ throw err;
233
+ }
179
234
  if (live) return { delivery: 'live' };
180
- // Offline: re-read so a write since the lookup above isn't reverted.
181
- const fresh = readList(root, org, 'approvals.json', 'approvals');
182
- const item = fresh.find((a) => a.requestId === requestId);
183
- if (!item || item.approved !== null)
184
- throw new HilError(409, `approval "${requestId}" was resolved meanwhile`);
185
- const now = Date.now();
186
- Object.assign(item, { approved, ts: now, resolvedBy: DASHBOARD_RESOLVER, resolvedAt: now });
187
- writeList(root, org, 'approvals.json', 'approvals', fresh);
188
- return { delivery: 'recorded' };
235
+ // Unlike gates and answers, nothing reads a recorded approval back: the
236
+ // request lived in the daemon's memory, and a new run asks again.
237
+ throw new HilError(409, `no running daemon hosts org "${org}" for this project — ${ended}`);
189
238
  }
190
239
 
191
240
  export async function resolveGate(root, org, gateId, approved, resolution) {
@@ -193,7 +242,7 @@ export async function resolveGate(root, org, gateId, approved, resolution) {
193
242
  if (!gate) throw new HilError(404, `gate "${gateId}" not found for org "${org}"`);
194
243
  if (gate.status !== 'pending')
195
244
  throw new HilError(409, `gate "${gateId}" already resolved (${gate.status})`);
196
- const live = await callDaemon(org, '/api/resolve-gate', {
245
+ const live = await callDaemon(root, org, '/api/resolve-gate', {
197
246
  org,
198
247
  gateId,
199
248
  approved,
@@ -223,7 +272,7 @@ export async function answerQuestion(root, org, questionId, answer) {
223
272
  if (!q) throw new HilError(404, `question "${questionId}" not found for org "${org}"`);
224
273
  if (q.answer !== null && q.answer !== undefined)
225
274
  throw new HilError(409, `question "${questionId}" was already answered`);
226
- const live = await callDaemon(org, '/api/answer-question', {
275
+ const live = await callDaemon(root, org, '/api/answer-question', {
227
276
  org,
228
277
  role: q.role,
229
278
  questionId,
@@ -257,7 +306,7 @@ export async function answerQuestion(root, org, questionId, answer) {
257
306
  /** Deliver `text` to `role`: live into its mailbox, else into the org's
258
307
  * inbox, which the daemon drains when the org next starts. */
259
308
  export async function sendHumanMessage(root, org, role, text) {
260
- const live = await callDaemon(org, '/api/human-message', { org, role, text });
309
+ const live = await callDaemon(root, org, '/api/human-message', { org, role, text });
261
310
  if (live) return { delivery: 'live' };
262
311
  const queued = queueMessage(root, org, {
263
312
  fromQualified: 'human',
@@ -11,16 +11,17 @@
11
11
  // - the org definition, resolved through cost_tiers like session.ts does.
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
- // Compiled from orgrt/*.ts, like forwarder.js in routes-org.mjs.
15
- import { lookupOrg } from '../orgrt/broker.js';
16
14
  import { resolveRoleCostTier, validateCostTiers } from '../orgrt/cost-tier.js';
17
15
  import { readIdleStatus } from '../orgrt/idle-deadline.js';
18
16
  import { OrgDefSchema } from '../orgrt/types.js';
17
+ import { hostingDaemon, writeFileAtomic } from './org-hil.mjs';
19
18
 
20
19
  const orgDir = (root, org) => path.join(root, '.monomind', 'orgs', org);
21
20
 
22
- /** Audit reasons the Runtime pane lists: the completion-evidence gate, the
23
- * idle watchdog, org_complete refusals and role failures. */
21
+ /** Reasons the Runtime pane lists: the completion-evidence gate, the idle
22
+ * watchdog, org_complete refusals and role failures. The runtime emits some
23
+ * as 'audit' events and some (budget exhaustion, agent-fatal, loadout
24
+ * mismatch) as 'status' events, so both types are read. */
24
25
  export const RUNTIME_AUDIT_REASONS = new Set([
25
26
  'task-evidence-refused',
26
27
  'task-evidence-escalated',
@@ -37,6 +38,11 @@ export const RUNTIME_AUDIT_REASONS = new Set([
37
38
  'session-result-error',
38
39
  'loadout-unresolvable',
39
40
  'loadout-mismatch',
41
+ 'agent-session-crash',
42
+ 'agent-context-limit',
43
+ 'boss-context-limit',
44
+ 'boss-restart',
45
+ 'boss-restart-exhausted',
40
46
  ]);
41
47
  const MAX_AUDIT = 60;
42
48
 
@@ -106,7 +112,7 @@ export function readRunDigest(root, org, run) {
106
112
  u.cache_creation += Number(d.cache_creation) || 0;
107
113
  u.cost_usd += Number(d.cost_usd) || 0;
108
114
  u.turns += 1;
109
- } else if (e.type === 'audit' && RUNTIME_AUDIT_REASONS.has(e.reason)) {
115
+ } else if ((e.type === 'audit' || e.type === 'status') && RUNTIME_AUDIT_REASONS.has(e.reason)) {
110
116
  audit.push({
111
117
  ts: e.ts,
112
118
  reason: e.reason,
@@ -122,9 +128,10 @@ export function readRunDigest(root, org, run) {
122
128
  return digest;
123
129
  }
124
130
 
125
- /** The live daemon's status for `org`, or null when no daemon hosts it. */
126
- async function liveStatus(org) {
127
- const entry = lookupOrg(org);
131
+ /** The live daemon's status for `org`, or null when no daemon hosts it for
132
+ * `root` (a same-named org in another project is not this one). */
133
+ async function liveStatus(root, org) {
134
+ const entry = hostingDaemon(root, org);
128
135
  if (!entry) return null;
129
136
  try {
130
137
  const res = await fetch(`${entry.url}/api/status`, {
@@ -208,7 +215,7 @@ export async function runtimeView(root, org) {
208
215
  const def = parsed.success ? parsed.data : { ...raw, run_config: raw.run_config ?? {} };
209
216
  const rc = def.run_config ?? {};
210
217
  const runtime = readJson(path.join(orgDir(root, org), 'runtime.json'));
211
- const live = await liveStatus(org);
218
+ const live = await liveStatus(root, org);
212
219
  const isLive = !!(live && !live.unreachable);
213
220
  const run = (isLive ? live.run : null) ?? runtime?.run ?? null;
214
221
  const digest = run ? readRunDigest(root, org, run) : null;
@@ -314,7 +321,10 @@ export function patchOrgConfig(root, org, patch) {
314
321
  }
315
322
  const problems = [];
316
323
  const next = { ...raw };
317
- if (patch.goal !== undefined) next.goal = String(patch.goal);
324
+ if (patch.goal !== undefined) {
325
+ if (typeof patch.goal === 'string') next.goal = patch.goal;
326
+ else problems.push('goal must be a string');
327
+ }
318
328
  if (patch.schedule !== undefined) next.schedule = patch.schedule === '' ? null : patch.schedule;
319
329
  if (patch.run_config !== undefined) {
320
330
  const rc = { ...(raw.run_config ?? {}) };
@@ -343,8 +353,6 @@ export function patchOrgConfig(root, org, patch) {
343
353
  );
344
354
  const tierProblems = validateCostTiers(parsed.data);
345
355
  if (tierProblems.length) throw new ConfigRejected(tierProblems);
346
- const tmp = `${file}.${process.pid}.tmp`;
347
- fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
348
- fs.renameSync(tmp, file);
356
+ writeFileAtomic(file, `${JSON.stringify(next, null, 2)}\n`);
349
357
  return next;
350
358
  }