@monoes/monomindcli 2.12.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 (185) hide show
  1. package/.claude/skills/mastermind/SKILL.md +1 -15
  2. package/README.md +1 -1
  3. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  4. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  5. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  6. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  7. package/dist/src/commands/doctor-monoes-checks.d.ts +1 -0
  8. package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
  9. package/dist/src/commands/doctor-monoes-checks.js +96 -34
  10. package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
  11. package/dist/src/commands/doctor-project-checks.d.ts +31 -1
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +109 -4
  14. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  15. package/dist/src/commands/doctor.d.ts.map +1 -1
  16. package/dist/src/commands/doctor.js +21 -3
  17. package/dist/src/commands/doctor.js.map +1 -1
  18. package/dist/src/commands/init.d.ts.map +1 -1
  19. package/dist/src/commands/init.js +29 -0
  20. package/dist/src/commands/init.js.map +1 -1
  21. package/dist/src/commands/mcp.d.ts.map +1 -1
  22. package/dist/src/commands/mcp.js +4 -3
  23. package/dist/src/commands/mcp.js.map +1 -1
  24. package/dist/src/commands/org-observe.d.ts.map +1 -1
  25. package/dist/src/commands/org-observe.js +14 -0
  26. package/dist/src/commands/org-observe.js.map +1 -1
  27. package/dist/src/commands/org.d.ts.map +1 -1
  28. package/dist/src/commands/org.js +21 -4
  29. package/dist/src/commands/org.js.map +1 -1
  30. package/dist/src/commands/ui.d.ts.map +1 -1
  31. package/dist/src/commands/ui.js +4 -0
  32. package/dist/src/commands/ui.js.map +1 -1
  33. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  34. package/dist/src/init/claudemd-generator.js +2 -1
  35. package/dist/src/init/claudemd-generator.js.map +1 -1
  36. package/dist/src/init/copy-assets.d.ts.map +1 -1
  37. package/dist/src/init/copy-assets.js +56 -18
  38. package/dist/src/init/copy-assets.js.map +1 -1
  39. package/dist/src/init/executor.d.ts.map +1 -1
  40. package/dist/src/init/executor.js +9 -1
  41. package/dist/src/init/executor.js.map +1 -1
  42. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  43. package/dist/src/init/mcp-generator.js +3 -3
  44. package/dist/src/init/mcp-generator.js.map +1 -1
  45. package/dist/src/init/never-commit.d.ts +39 -0
  46. package/dist/src/init/never-commit.d.ts.map +1 -0
  47. package/dist/src/init/never-commit.js +15 -0
  48. package/dist/src/init/never-commit.js.map +1 -0
  49. package/dist/src/init/shared.d.ts +74 -1
  50. package/dist/src/init/shared.d.ts.map +1 -1
  51. package/dist/src/init/shared.js +166 -0
  52. package/dist/src/init/shared.js.map +1 -1
  53. package/dist/src/init/types.d.ts +11 -0
  54. package/dist/src/init/types.d.ts.map +1 -1
  55. package/dist/src/init/types.js.map +1 -1
  56. package/dist/src/init/upgrade.d.ts.map +1 -1
  57. package/dist/src/init/upgrade.js +1 -0
  58. package/dist/src/init/upgrade.js.map +1 -1
  59. package/dist/src/init/write-kimicode.d.ts.map +1 -1
  60. package/dist/src/init/write-kimicode.js +13 -9
  61. package/dist/src/init/write-kimicode.js.map +1 -1
  62. package/dist/src/init/write-opencode.d.ts.map +1 -1
  63. package/dist/src/init/write-opencode.js +44 -2
  64. package/dist/src/init/write-opencode.js.map +1 -1
  65. package/dist/src/init/write-runtime-config.d.ts +2 -0
  66. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  67. package/dist/src/init/write-runtime-config.js +65 -38
  68. package/dist/src/init/write-runtime-config.js.map +1 -1
  69. package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
  70. package/dist/src/mastermind/manifest-data.js +14 -0
  71. package/dist/src/mastermind/manifest-data.js.map +1 -1
  72. package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
  73. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  74. package/dist/src/mcp-tools/terminal-tools.js +52 -9
  75. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  76. package/dist/src/memory/memory-bridge.d.ts +84 -3
  77. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  78. package/dist/src/memory/memory-bridge.js +215 -17
  79. package/dist/src/memory/memory-bridge.js.map +1 -1
  80. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  81. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  82. package/dist/src/orgrt/agent-runner.js +47 -0
  83. package/dist/src/orgrt/agent-runner.js.map +1 -1
  84. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  85. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  86. package/dist/src/orgrt/checkpoint.js +15 -6
  87. package/dist/src/orgrt/checkpoint.js.map +1 -1
  88. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  89. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  90. package/dist/src/orgrt/completion-gate.js +45 -0
  91. package/dist/src/orgrt/completion-gate.js.map +1 -1
  92. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  93. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  94. package/dist/src/orgrt/cost-tier.js +183 -0
  95. package/dist/src/orgrt/cost-tier.js.map +1 -0
  96. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  97. package/dist/src/orgrt/cross-org.js +14 -0
  98. package/dist/src/orgrt/cross-org.js.map +1 -1
  99. package/dist/src/orgrt/daemon.d.ts +12 -1
  100. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  101. package/dist/src/orgrt/daemon.js +216 -49
  102. package/dist/src/orgrt/daemon.js.map +1 -1
  103. package/dist/src/orgrt/decisions.d.ts +14 -2
  104. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  105. package/dist/src/orgrt/decisions.js +216 -10
  106. package/dist/src/orgrt/decisions.js.map +1 -1
  107. package/dist/src/orgrt/file-roots.d.ts +7 -0
  108. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  109. package/dist/src/orgrt/file-roots.js +38 -1
  110. package/dist/src/orgrt/file-roots.js.map +1 -1
  111. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  112. package/dist/src/orgrt/forwarder.js +16 -2
  113. package/dist/src/orgrt/forwarder.js.map +1 -1
  114. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  115. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  116. package/dist/src/orgrt/idle-deadline.js +52 -2
  117. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  118. package/dist/src/orgrt/loadouts.d.ts +38 -0
  119. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  120. package/dist/src/orgrt/loadouts.js +129 -0
  121. package/dist/src/orgrt/loadouts.js.map +1 -0
  122. package/dist/src/orgrt/mailbox.d.ts +17 -1
  123. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  124. package/dist/src/orgrt/mailbox.js +50 -3
  125. package/dist/src/orgrt/mailbox.js.map +1 -1
  126. package/dist/src/orgrt/policy.d.ts +42 -2
  127. package/dist/src/orgrt/policy.d.ts.map +1 -1
  128. package/dist/src/orgrt/policy.js +53 -8
  129. package/dist/src/orgrt/policy.js.map +1 -1
  130. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  131. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  132. package/dist/src/orgrt/prompt-vars.js +49 -0
  133. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  134. package/dist/src/orgrt/questions.d.ts +22 -1
  135. package/dist/src/orgrt/questions.d.ts.map +1 -1
  136. package/dist/src/orgrt/questions.js +24 -3
  137. package/dist/src/orgrt/questions.js.map +1 -1
  138. package/dist/src/orgrt/review-packet.d.ts +29 -0
  139. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  140. package/dist/src/orgrt/review-packet.js +67 -0
  141. package/dist/src/orgrt/review-packet.js.map +1 -0
  142. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  143. package/dist/src/orgrt/role-sandbox.js +16 -3
  144. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  145. package/dist/src/orgrt/session-ledger.d.ts +61 -0
  146. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  147. package/dist/src/orgrt/session-ledger.js +113 -0
  148. package/dist/src/orgrt/session-ledger.js.map +1 -0
  149. package/dist/src/orgrt/session.d.ts +45 -5
  150. package/dist/src/orgrt/session.d.ts.map +1 -1
  151. package/dist/src/orgrt/session.js +384 -27
  152. package/dist/src/orgrt/session.js.map +1 -1
  153. package/dist/src/orgrt/task-dag.d.ts +34 -1
  154. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  155. package/dist/src/orgrt/task-dag.js +58 -7
  156. package/dist/src/orgrt/task-dag.js.map +1 -1
  157. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  158. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  159. package/dist/src/orgrt/tool-spill.js +180 -0
  160. package/dist/src/orgrt/tool-spill.js.map +1 -0
  161. package/dist/src/orgrt/types.d.ts +138 -0
  162. package/dist/src/orgrt/types.d.ts.map +1 -1
  163. package/dist/src/orgrt/types.js +148 -0
  164. package/dist/src/orgrt/types.js.map +1 -1
  165. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  166. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  167. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  168. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  169. package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
  170. package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
  171. package/dist/src/platform-adapters/renderers/skills.js +12 -22
  172. package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
  173. package/dist/src/protocol-capabilities.d.ts +2 -1
  174. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  175. package/dist/src/protocol-capabilities.js +2 -1
  176. package/dist/src/protocol-capabilities.js.map +1 -1
  177. package/dist/src/ui/dashboard.html +457 -164
  178. package/dist/src/ui/org-hil.mjs +276 -0
  179. package/dist/src/ui/org-runtime.mjs +350 -0
  180. package/dist/src/ui/orgs.html +22 -4
  181. package/dist/src/ui/routes-monoes.mjs +28 -3
  182. package/dist/src/ui/routes-org.mjs +290 -541
  183. package/dist/src/ui/server.mjs +185 -19
  184. package/dist/tsconfig.tsbuildinfo +1 -1
  185. package/package.json +7 -6
@@ -1646,7 +1646,7 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1646
1646
  <div class="nav-item" data-view="global-tokens" title="Token usage across all projects">
1647
1647
  <span class="ico">$</span><span class="lbl">Global Tokens</span>
1648
1648
  </div>
1649
- <div class="nav-item" data-view="humaninput" title="Pending questions from agents across all orgs">
1649
+ <div class="nav-item" data-view="humaninput" title="Questions, approvals and decision gates waiting on you, across all orgs">
1650
1650
  <span class="ico">❓</span><span class="lbl">Human Input</span>
1651
1651
  <span class="bdg" id="bdg-humaninput">—</span>
1652
1652
  </div>
@@ -2094,6 +2094,7 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2094
2094
  <div id="org-detail-tabs">
2095
2095
  <button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
2096
2096
  <button class="odt-btn" data-tab="chat" onclick="v2SwitchOrgTab('chat')">Chat</button>
2097
+ <button class="odt-btn" data-tab="runtime" onclick="v2SwitchOrgTab('runtime')">Runtime</button>
2097
2098
  <div class="odt-tab-sep" title="Secondary tabs"></div>
2098
2099
  <button class="odt-btn secondary" data-tab="config" onclick="v2SwitchOrgTab('config')">Config</button>
2099
2100
  <button class="odt-btn secondary" data-tab="files" onclick="v2SwitchOrgTab('files')">Files</button>
@@ -2103,6 +2104,7 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2103
2104
  <div id="org-detail-body">
2104
2105
  <div class="odt-pane active" id="odt-chart"></div>
2105
2106
  <div class="odt-pane" id="odt-chat" style="flex-direction:column;height:100%;overflow:hidden;"></div>
2107
+ <div class="odt-pane" id="odt-runtime" style="overflow-y:auto"></div>
2106
2108
  <div class="odt-pane" id="odt-config" style="overflow-y:auto"></div>
2107
2109
  <div class="odt-pane" id="odt-files" style="overflow-y:auto"></div>
2108
2110
  <div class="odt-pane" id="odt-approvals"></div>
@@ -2303,11 +2305,11 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2303
2305
  <div class="vscroll">
2304
2306
  <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
2305
2307
  <div class="pg-title" style="margin-bottom:0">Human Input</div>
2306
- <span class="pg-sub" id="hi-sub" style="margin-bottom:0">Pending questions across all projects</span>
2308
+ <span class="pg-sub" id="hi-sub" style="margin-bottom:0">Questions, approvals and decision gates waiting on you</span>
2307
2309
  </div>
2308
2310
  <div class="hi-tab-bar" style="display:flex;gap:4px;margin-top:12px;border-bottom:1px solid var(--border);padding-bottom:6px">
2309
2311
  <button class="odt-btn active" data-hitab="pending" onclick="hiSwitchTab('pending')">Pending <span style="font-size:10px;background:var(--surface-hi);color:var(--text-lo);border-radius:8px;padding:1px 6px;min-width:18px;display:inline-block;text-align:center" id="hi-tab-count-pending">—</span></button>
2310
- <button class="odt-btn" data-hitab="answered" onclick="hiSwitchTab('answered')">Answered <span style="font-size:10px;background:var(--surface-hi);color:var(--text-lo);border-radius:8px;padding:1px 6px;min-width:18px;display:inline-block;text-align:center" id="hi-tab-count-answered">—</span></button>
2312
+ <button class="odt-btn" data-hitab="answered" onclick="hiSwitchTab('answered')">Resolved <span style="font-size:10px;background:var(--surface-hi);color:var(--text-lo);border-radius:8px;padding:1px 6px;min-width:18px;display:inline-block;text-align:center" id="hi-tab-count-answered">—</span></button>
2311
2313
  </div>
2312
2314
  <div id="hi-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2313
2315
  </div>
@@ -2446,6 +2448,13 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2446
2448
  // automatically. EventSource can't carry custom headers at all, so SSE call
2447
2449
  // sites must use window.mmEventSource(url) instead of `new EventSource(url)`
2448
2450
  // directly — it appends the credential as a query param.
2451
+ //
2452
+ // i-073: the query param is accepted for GET/HEAD ONLY — a POST/PUT/DELETE
2453
+ // with only ?token= is a CORS-simple request (no preflight, reachable from a
2454
+ // plain <form>), so mutations MUST send the header. Any new call that
2455
+ // mutates has to go through fetch() (the wrapper above), never a hand-built
2456
+ // URL with the token appended — that would silently reopen the CSRF hole
2457
+ // this item closed.
2449
2458
  (() => {
2450
2459
  const AUTH_HEADER_NAME = 'x-monomind-' + 'token';
2451
2460
  const AUTH_QUERY_PARAM = 'to' + 'ken';
@@ -2620,6 +2629,8 @@ async function init() {
2620
2629
  sbProjEl.title = DIR;
2621
2630
  _showNavProjectCtx(DIR);
2622
2631
  } catch (_) {}
2632
+ // Count what is waiting on a human now, not only once the view is opened.
2633
+ _hiScheduleReload();
2623
2634
  // deep-link: ?sess=<id>&proj=<path>
2624
2635
  const params = new URLSearchParams(window.location.search);
2625
2636
  let projParam = params.get('proj');
@@ -4439,38 +4450,57 @@ async function _gdCopyPath(p) {
4439
4450
  // renderDocMarkdown() and gdCopyCode() moved to markdown.js (#124) — loaded via
4440
4451
  // <script src="markdown.js"> above; both remain plain globals.
4441
4452
 
4453
+ // Human Input: everything an org is waiting on a human for — ask_human
4454
+ // questions, tool/action approvals and decision gates — across projects.
4455
+ // Each entry: { kind: 'question'|'approval'|'gate', project, item }.
4442
4456
  let _hiEntries = [];
4443
4457
  let _hiTab = 'pending';
4444
4458
 
4459
+ function _hiIsPending(e) {
4460
+ if (e.kind === 'question') return e.item.answer === null || e.item.answer === undefined;
4461
+ return (e.item.status || 'pending') === 'pending';
4462
+ }
4463
+ function _hiWhen(e) {
4464
+ const i = e.item;
4465
+ return i.answeredAt || i.resolvedAt || i.ts || i.createdAt || 0;
4466
+ }
4467
+
4445
4468
  async function renderHumanInputView() {
4446
4469
  const el = document.getElementById('hi-content');
4447
- el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
4470
+ if (!_hiEntries.length) el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
4448
4471
  try {
4449
4472
  const data = await apiFetch('/api/projects');
4450
4473
  const projects = (data?.projects || []).slice(0, 8);
4451
- if (!projects.length) {
4452
- el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No projects found</div></div>';
4453
- _hiEntries = [];
4454
- updateHumanInputBadge(0);
4455
- renderHiCurrentTab(projects.length);
4456
- return;
4457
- }
4474
+ // The project this dashboard serves always counts, even when it is not
4475
+ // among the most recently active Claude projects.
4476
+ if (DIR && !projects.some(p => p.path === DIR)) projects.unshift({ path: DIR, name: baseName(DIR) });
4458
4477
  const results = await Promise.allSettled(
4459
- projects.map(p => apiFetch(`/api/questions?dir=${enc(p.path)}`).then(d => ({ project: p, questions: d.questions || [] })))
4478
+ projects.map(p => apiFetch(`/api/hil?dir=${enc(p.path)}`).then(d => ({ project: p, d: d || {} })))
4460
4479
  );
4461
4480
  const entries = [];
4481
+ const errors = [];
4462
4482
  for (const r of results) {
4463
4483
  if (r.status !== 'fulfilled') continue;
4464
- const { project, questions } = r.value;
4465
- for (const q of questions) entries.push({ project, q });
4484
+ const { project, d } = r.value;
4485
+ for (const item of d.questions || []) entries.push({ kind: 'question', project, item });
4486
+ for (const item of d.approvals || []) if (item.id) entries.push({ kind: 'approval', project, item });
4487
+ for (const item of d.gates || []) entries.push({ kind: 'gate', project, item });
4488
+ for (const e of d.errors || []) errors.push(`${project.name || project.path}/${e.org}: ${e.error}`);
4466
4489
  }
4467
- entries.sort((a, b) => (b.q.answeredAt || b.q.ts || 0) - (a.q.answeredAt || a.q.ts || 0));
4490
+ entries.sort((a, b) => _hiWhen(b) - _hiWhen(a));
4468
4491
  _hiEntries = entries;
4469
- updateHumanInputBadge(entries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
4470
- renderHiCurrentTab(projects.length);
4492
+ updateHumanInputBadge(entries.filter(_hiIsPending).length);
4493
+ renderHiCurrentTab(projects.length, errors);
4471
4494
  } catch (e) { el.innerHTML = `<div class="empty">Failed: ${esc(e.message)}</div>`; }
4472
4495
  }
4473
4496
 
4497
+ // Coalesce bursts of org:question / org:gate / resolution events into one reload.
4498
+ let _hiReloadTimer = null;
4499
+ function _hiScheduleReload() {
4500
+ clearTimeout(_hiReloadTimer);
4501
+ _hiReloadTimer = setTimeout(renderHumanInputView, 400);
4502
+ }
4503
+
4474
4504
  function _hiSwitchTab(tab) {
4475
4505
  _hiTab = tab;
4476
4506
  document.querySelectorAll('.hi-tab-bar .odt-btn').forEach(b =>
@@ -4478,39 +4508,80 @@ function _hiSwitchTab(tab) {
4478
4508
  renderHiCurrentTab();
4479
4509
  }
4480
4510
 
4481
- function renderHiCurrentTab(projectCount) {
4511
+ const _HI_KIND_LABEL = { question: 'Question', approval: 'Approval', gate: 'Decision gate' };
4512
+
4513
+ function _hiPill(text, color) {
4514
+ return `<span class="ss-pill" style="color:${color};border-color:${color}">${esc(text)}</span>`;
4515
+ }
4516
+
4517
+ function _hiEntryHtml(e) {
4518
+ const i = e.item;
4519
+ const pending = _hiIsPending(e);
4520
+ const data = `data-kind="${e.kind}" data-dir="${esc(e.project.path)}" data-org="${esc(i.org)}" data-id="${esc(e.kind === 'question' ? i.questionId : i.id)}"`;
4521
+ let badge = '';
4522
+ let body = '';
4523
+ let resolved = '';
4524
+ let actions = '';
4525
+ if (e.kind === 'question') {
4526
+ // Only a pending question can hold a run; once answered the flag is history.
4527
+ if (pending) badge = i.blocking === false
4528
+ ? _hiPill('non-blocking', 'var(--text-lo)')
4529
+ : _hiPill('blocking — run waits', 'var(--amber, #d29922)');
4530
+ body = `<div style="color:var(--text-hi);white-space:pre-wrap;max-height:240px;overflow:auto">${esc(i.question)}</div>`;
4531
+ resolved = `<span style="color:var(--text-xs)">Answer:</span> ${esc(i.answer ?? '')}`;
4532
+ actions = `<input type="text" class="hi-answer-input" placeholder="Your answer…" aria-label="Answer" style="flex:1;padding:4px 8px;font-size:12px" />
4533
+ <button class="btn" style="font-size:11px" ${data} onclick="hiAct(this,'answer')">Answer</button>`;
4534
+ } else if (e.kind === 'approval') {
4535
+ badge = _hiPill(i.action || 'tool', 'var(--accent, #58a6ff)');
4536
+ body = `<div style="color:var(--text-hi)">Wants to run <b>${esc(i.action)}</b></div>` +
4537
+ (i.input ? `<pre style="font-size:11px;white-space:pre-wrap;max-height:160px;overflow:auto;margin:0;color:var(--text-lo)">${esc(JSON.stringify(i.input, null, 2))}</pre>` : '');
4538
+ resolved = `${_hiPill(i.status, i.status === 'approved' ? 'var(--green)' : 'var(--red)')}`;
4539
+ actions = `<button class="btn" style="font-size:11px;color:var(--green);border-color:var(--green)" ${data} onclick="hiAct(this,'approve')">Approve</button>
4540
+ <button class="btn" style="font-size:11px;color:var(--red);border-color:var(--red)" ${data} onclick="hiAct(this,'reject')">Deny</button>`;
4541
+ } else {
4542
+ body = `<div style="color:var(--text-hi);font-weight:600">${esc(i.name || '')}</div>
4543
+ <div style="color:var(--text-lo);white-space:pre-wrap;max-height:240px;overflow:auto">${esc(i.description || '')}</div>`;
4544
+ resolved = `${_hiPill(i.status, i.status === 'approved' ? 'var(--green)' : 'var(--red)')}${i.resolution ? ` ${esc(i.resolution)}` : ''}`;
4545
+ actions = `<input type="text" class="hi-answer-input" placeholder="Resolution note (optional)…" aria-label="Resolution note" style="flex:1;padding:4px 8px;font-size:12px" />
4546
+ <button class="btn" style="font-size:11px;color:var(--green);border-color:var(--green)" ${data} onclick="hiAct(this,'approve')">Approve</button>
4547
+ <button class="btn" style="font-size:11px;color:var(--red);border-color:var(--red)" ${data} onclick="hiAct(this,'reject')">Reject</button>`;
4548
+ }
4549
+ const who = i.resolvedBy ? ` · by ${esc(i.resolvedBy)}` : '';
4550
+ return `<div class="sess-row" style="flex-direction:column;align-items:stretch;gap:6px">
4551
+ <div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
4552
+ <div><span style="color:var(--text-xs)">${_HI_KIND_LABEL[e.kind]}</span> · <b>${esc(i.org)}</b> · ${esc(i.role || i.roleId || '')} ${badge}
4553
+ <span style="color:var(--text-xs)">(${esc(e.project.name || e.project.path)})</span></div>
4554
+ <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(i.ts || i.createdAt)}</div>
4555
+ </div>
4556
+ ${body}
4557
+ ${pending
4558
+ ? `<div style="display:flex;gap:6px;align-items:center">${actions}</div>`
4559
+ : `<div style="display:flex;justify-content:space-between;gap:8px;padding:6px 8px;background:var(--surface-hi);border-radius:4px">
4560
+ <div style="white-space:pre-wrap;max-height:160px;overflow:auto;min-width:0">${resolved}</div>
4561
+ <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono);flex-shrink:0">${relTime(i.answeredAt || i.resolvedAt)}${who}</div>
4562
+ </div>`}
4563
+ </div>`;
4564
+ }
4565
+
4566
+ function renderHiCurrentTab(projectCount, errors) {
4482
4567
  const el = document.getElementById('hi-content');
4483
- const pending = _hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined);
4484
- const answered = _hiEntries.filter(({ q }) => q.answer !== null && q.answer !== undefined);
4568
+ const pending = _hiEntries.filter(_hiIsPending);
4569
+ const done = _hiEntries.filter(e => !_hiIsPending(e));
4485
4570
  document.getElementById('hi-tab-count-pending').textContent = pending.length || '—';
4486
- document.getElementById('hi-tab-count-answered').textContent = answered.length || '—';
4487
- const entries = _hiTab === 'answered' ? answered : pending;
4571
+ document.getElementById('hi-tab-count-answered').textContent = done.length || '—';
4572
+ const entries = _hiTab === 'answered' ? done : pending;
4488
4573
  const projTxt = projectCount != null ? ` across ${projectCount} project${projectCount !== 1 ? 's' : ''}` : '';
4574
+ const blocking = pending.filter(e => e.kind !== 'question' || e.item.blocking !== false).length;
4489
4575
  document.getElementById('hi-sub').textContent = _hiTab === 'answered'
4490
- ? (answered.length ? `${answered.length} answered question(s)${projTxt}` : `No answered questions${projTxt}`)
4491
- : (pending.length ? `${pending.length} pending question(s)${projTxt}` : `No pending questions${projTxt}`);
4576
+ ? (done.length ? `${done.length} resolved${projTxt}` : `Nothing resolved yet${projTxt}`)
4577
+ : (pending.length ? `${pending.length} waiting on you (${blocking} holding a run)${projTxt}` : `Nothing waiting on you${projTxt}`);
4578
+ const errHtml = (errors || []).length
4579
+ ? `<div class="empty" style="color:var(--red)">Unreadable: ${errors.map(esc).join('; ')}</div>` : '';
4492
4580
  if (!entries.length) {
4493
- el.innerHTML = `<div class="empty"><div class="empty-ico">❓</div><div>No ${_hiTab} questions</div></div>`;
4581
+ el.innerHTML = `${errHtml}<div class="empty"><div class="empty-ico">❓</div><div>${_hiTab === 'answered' ? 'Nothing resolved yet' : 'Nothing waiting on you'}</div></div>`;
4494
4582
  return;
4495
4583
  }
4496
- el.innerHTML = `<div class="sess-list">${entries.map(({ project, q }) => `
4497
- <div class="sess-row" data-question-id="${esc(q.questionId)}" style="flex-direction:column;align-items:stretch;gap:6px">
4498
- <div style="display:flex;justify-content:space-between;gap:8px">
4499
- <div><b>${esc(q.org)}</b> · ${esc(q.role)} <span style="color:var(--text-xs)">(${esc(project.name || project.path)})</span></div>
4500
- <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(q.ts)}</div>
4501
- </div>
4502
- <div style="color:var(--text-hi)">${esc(q.question)}</div>
4503
- ${_hiTab === 'answered' ? `
4504
- <div style="display:flex;justify-content:space-between;gap:8px;padding:6px 8px;background:var(--surface-hi);border-radius:4px">
4505
- <div><span style="color:var(--text-xs)">Answer:</span> ${esc(q.answer)}</div>
4506
- <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono);flex-shrink:0">${relTime(q.answeredAt)}</div>
4507
- </div>` : `
4508
- <div style="display:flex;gap:6px">
4509
- <input type="text" class="hi-answer-input" placeholder="Your answer…" style="flex:1;padding:4px 8px;font-size:12px" />
4510
- <button class="btn" style="font-size:11px" onclick="submitHumanAnswer('${esc(String(project.path).replace(/['\\]/g,'\\$&'))}','${esc(String(q.org).replace(/['\\]/g,'\\$&'))}','${esc(String(q.role).replace(/['\\]/g,'\\$&'))}','${esc(String(q.questionId).replace(/['\\]/g,'\\$&'))}',this)">Answer</button>
4511
- </div>`}
4512
- </div>
4513
- `).join('')}</div>`;
4584
+ el.innerHTML = `${errHtml}<div class="sess-list">${entries.map(_hiEntryHtml).join('')}</div>`;
4514
4585
  }
4515
4586
 
4516
4587
  function updateHumanInputBadge(count) {
@@ -4518,26 +4589,38 @@ function updateHumanInputBadge(count) {
4518
4589
  if (b) b.textContent = String(count);
4519
4590
  }
4520
4591
 
4521
- async function _submitHumanAnswer(dir, org, role, questionId, btnEl) {
4522
- const row = btnEl.closest('[data-question-id]');
4523
- const input = row.querySelector('.hi-answer-input');
4524
- const answer = input.value.trim();
4525
- if (!answer) return;
4526
- btnEl.disabled = true;
4592
+ /** POST a human decision; toasts the outcome. Resolves to the response data, or null on failure. */
4593
+ async function _hilPost(url, body) {
4527
4594
  try {
4528
- const res = await fetch('/api/questions/answer', {
4529
- method: 'POST', headers: { 'Content-Type': 'application/json' },
4530
- body: JSON.stringify({ dir, org, role, questionId, answer }),
4595
+ const res = await fetch(url, {
4596
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
4531
4597
  });
4532
- const data = await res.json();
4533
- if (data.ok) {
4534
- showToast('Answered', `${org}:${role}`, 'ok');
4535
- const entry = _hiEntries.find(({ q }) => q.questionId === questionId);
4536
- if (entry) { entry.q.answer = answer; entry.q.answeredAt = Date.now(); }
4537
- updateHumanInputBadge(_hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
4538
- renderHiCurrentTab();
4539
- } else { showToast('Error', data.error || 'failed', 'err'); btnEl.disabled = false; }
4540
- } catch (e) { showToast('Error', e.message, 'err'); btnEl.disabled = false; }
4598
+ const data = await res.json().catch(() => ({}));
4599
+ if (!res.ok || !data.ok) { showToast('Not delivered', data.error || `HTTP ${res.status}`, 'err'); return null; }
4600
+ showToast('Done', data.delivery === 'live'
4601
+ ? 'Delivered to the running org'
4602
+ : 'Recorded — the org picks it up when it next runs', 'ok');
4603
+ return data;
4604
+ } catch (e) { showToast('Error', e.message, 'err'); return null; }
4605
+ }
4606
+
4607
+ async function _hiAct(btnEl, verb) {
4608
+ const { kind, dir, org, id } = btnEl.dataset;
4609
+ const row = btnEl.closest('.sess-row');
4610
+ const text = row?.querySelector('.hi-answer-input')?.value.trim() || '';
4611
+ const q = `?dir=${enc(dir)}`;
4612
+ let ok = null;
4613
+ row?.querySelectorAll('button').forEach(b => { b.disabled = true; });
4614
+ if (kind === 'question') {
4615
+ if (!text) { row?.querySelectorAll('button').forEach(b => { b.disabled = false; }); return; }
4616
+ ok = await _hilPost('/api/questions/answer', { dir, org, questionId: id, answer: text });
4617
+ } else if (kind === 'approval') {
4618
+ ok = await _hilPost(`/api/org/${enc(org)}/approvals/${enc(id)}${q}`, { action: verb });
4619
+ } else if (kind === 'gate') {
4620
+ ok = await _hilPost(`/api/org/${enc(org)}/gates/${enc(id)}${q}`, { approved: verb === 'approve', resolution: text || undefined });
4621
+ }
4622
+ if (!ok) { row?.querySelectorAll('button').forEach(b => { b.disabled = false; }); return; }
4623
+ renderHumanInputView();
4541
4624
  }
4542
4625
 
4543
4626
  function _cvExcerptText(ev) {
@@ -5902,6 +5985,7 @@ function v2RenderOrgTab(tab) {
5902
5985
  return;
5903
5986
  }
5904
5987
  if (tab === 'chart') v2RenderOrgChart();
5988
+ else if (tab === 'runtime') v2RenderOrgRuntime();
5905
5989
  else if (tab === 'roles') v2RenderOrgRoles();
5906
5990
  else if (tab === 'health') v2RenderOrgHealth();
5907
5991
  else if (tab === 'heartbeats') v2RenderOrgHeartbeats();
@@ -7114,34 +7198,38 @@ function v2RenderOrgLive() {
7114
7198
  }
7115
7199
 
7116
7200
  // ── APPROVALS ──────────────────────────────────────────────
7201
+ // Tool/action approvals (<org>/approvals.json): a role asked to run a
7202
+ // sensitive tool (Bash, WebFetch, org_complete, policy.approvalTools…).
7117
7203
  async function v2RenderOrgApprovals() {
7118
7204
  const el = document.getElementById('odt-approvals');
7119
7205
  if (!el || !_v2SelOrg) return;
7120
7206
  const _rendOrg = _v2SelOrg;
7121
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
7207
+ if (!el.querySelector('table')) el.innerHTML = '<div class="loading-txt">Loading…</div>';
7122
7208
  try {
7123
7209
  const _enc = encodeURIComponent(_rendOrg);
7124
- const data = await fetch(`/api/org/${_enc}/approvals${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
7210
+ const res = await fetch(`/api/org/${_enc}/approvals${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`);
7211
+ const data = await res.json().catch(() => ({}));
7125
7212
  if (_v2SelOrg !== _rendOrg) return;
7126
- const approvals = Array.isArray(data) ? data : (data.approvals || []);
7127
- if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
7213
+ if (!res.ok) { el.innerHTML = `<div class="empty">Failed: ${esc(data.error || `HTTP ${res.status}`)}</div>`; return; }
7214
+ const approvals = data.approvals || [];
7215
+ if (!approvals.length) { el.innerHTML = '<div class="empty">No approval requests</div>'; return; }
7128
7216
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
7129
7217
  <thead><tr style="color:var(--text-xs);text-align:left">
7130
- <th style="padding:6px 8px">Requester</th><th>Action</th><th>Status</th><th>Date</th><th></th>
7218
+ <th style="padding:6px 8px">Role</th><th>Action</th><th>Input</th><th>Status</th><th>When</th><th></th>
7131
7219
  </tr></thead><tbody>` +
7132
- approvals.slice(0, 50).map(a => {
7133
- const cls = a.status === 'approved' ? 'on' : a.status === 'rejected' ? 'warn' : '';
7134
- const pending = !a.status || a.status === 'pending';
7135
- const aid = a.id || '';
7220
+ approvals.slice(0, 100).map(a => {
7221
+ const cls = a.status === 'approved' ? 'on' : a.status === 'denied' ? 'warn' : '';
7222
+ const input = a.input ? JSON.stringify(a.input) : '';
7136
7223
  return `<tr style="border-top:1px solid var(--border)">
7137
- <td style="padding:6px 8px;color:var(--text-hi)">${esc(a.requester || a.agent || '—')}</td>
7138
- <td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((a.action || a.description || '').slice(0, 80))}</td>
7139
- <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status || 'pending')}</span></td>
7140
- <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
7224
+ <td style="padding:6px 8px;color:var(--text-hi)">${esc(a.roleId || '—')}</td>
7225
+ <td style="padding:6px 8px;color:var(--text-hi);font-family:var(--mono)">${esc(a.action || '—')}</td>
7226
+ <td style="padding:6px 8px;color:var(--text-lo);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:11px" title="${esc(input)}">${esc(input.slice(0, 120) || '—')}</td>
7227
+ <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status)}</span>${a.resolvedBy ? `<div style="color:var(--text-xs);font-size:10px">by ${esc(a.resolvedBy)}</div>` : ''}</td>
7228
+ <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.resolvedAt || a.ts)}</td>
7141
7229
  <td style="padding:6px 8px;white-space:nowrap">
7142
- ${pending
7143
- ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'approve')">✓</button>
7144
- <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'reject')">✕</button>`
7230
+ ${a.status === 'pending' && a.id
7231
+ ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" data-aid="${esc(a.id)}" onclick="orgApprovalAction(this.dataset.aid,'approve')">✓</button>
7232
+ <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Deny" aria-label="Deny" data-aid="${esc(a.id)}" onclick="orgApprovalAction(this.dataset.aid,'reject')">✕</button>`
7145
7233
  : ''}
7146
7234
  </td>
7147
7235
  </tr>`;
@@ -7150,15 +7238,10 @@ async function v2RenderOrgApprovals() {
7150
7238
  }
7151
7239
 
7152
7240
  async function _orgApprovalAction(id, action) {
7153
- if (!confirm(`${action} this request?`)) return;
7154
- try {
7155
- const _enc = encodeURIComponent(_v2SelOrg);
7156
- await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`, {
7157
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
7158
- });
7159
- showToast('Done', `${action}d`, 'ok');
7160
- v2RenderOrgApprovals();
7161
- } catch (e) { showToast('Error', e.message, 'err'); }
7241
+ if (!confirm(`${action === 'approve' ? 'Approve' : 'Deny'} this request?`)) return;
7242
+ const _enc = encodeURIComponent(_v2SelOrg);
7243
+ const ok = await _hilPost(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`, { action });
7244
+ if (ok) v2RenderOrgApprovals();
7162
7245
  }
7163
7246
 
7164
7247
  // ── DECISIONS ──────────────────────────────────────────────
@@ -7203,20 +7286,35 @@ function v2RenderDecisionsTable() {
7203
7286
  if (!filtered.length) { el.innerHTML = `${filterBar}<div class="empty">No decision gates match</div>`; return; }
7204
7287
  el.innerHTML = filterBar + `<table style="width:100%;border-collapse:collapse;font-size:12px">
7205
7288
  <thead><tr style="color:var(--text-xs);text-align:left">
7206
- <th style="padding:6px 8px">Role</th><th>Gate</th><th>Status</th><th>Resolution</th><th>Date</th>
7289
+ <th style="padding:6px 8px">Role</th><th>Gate</th><th>Status</th><th>Resolution</th><th>Date</th><th></th>
7207
7290
  </tr></thead><tbody>` +
7208
7291
  filtered.slice(0, 100).map(g => {
7209
7292
  const cls = g.status === 'approved' ? 'on' : g.status === 'rejected' ? 'warn' : '';
7210
7293
  return `<tr style="border-top:1px solid var(--border)">
7211
7294
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(g.roleId || '—')}</td>
7212
7295
  <td style="padding:6px 8px;color:var(--text-lo);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(g.description || '')}">${esc(g.name || '—')}</td>
7213
- <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(g.status || 'pending')}</span></td>
7214
- <td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(g.resolution || '—')}</td>
7296
+ <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(g.status || 'pending')}</span>${g.resolvedBy ? `<div style="color:var(--text-xs);font-size:10px">by ${esc(g.resolvedBy)}</div>` : ''}</td>
7297
+ <td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(g.resolution || '')}">${esc(g.resolution || '—')}</td>
7215
7298
  <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(g.resolvedAt || g.createdAt)}</td>
7299
+ <td style="padding:6px 8px;white-space:nowrap">
7300
+ ${(g.status || 'pending') === 'pending'
7301
+ ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve gate" data-gid="${esc(g.id)}" onclick="orgGateAction(this.dataset.gid,true)">✓</button>
7302
+ <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject gate" data-gid="${esc(g.id)}" onclick="orgGateAction(this.dataset.gid,false)">✕</button>`
7303
+ : ''}
7304
+ </td>
7216
7305
  </tr>`;
7217
7306
  }).join('') + '</tbody></table>';
7218
7307
  }
7219
7308
 
7309
+ async function _orgGateAction(id, approved) {
7310
+ const gate = _v2DecisionsCache.find(g => g.id === id);
7311
+ const resolution = prompt(`${approved ? 'Approve' : 'Reject'} "${gate?.name || id}"?\nOptional note for the role:`, '');
7312
+ if (resolution === null) return;
7313
+ const _enc = encodeURIComponent(_v2SelOrg);
7314
+ const ok = await _hilPost(`/api/org/${_enc}/gates/${encodeURIComponent(id)}${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`, { approved, resolution: resolution.trim() || undefined });
7315
+ if (ok) v2RenderOrgDecisions();
7316
+ }
7317
+
7220
7318
  // ── SECRETS ────────────────────────────────────────────────
7221
7319
  async function v2RenderOrgSecrets() {
7222
7320
  const el = document.getElementById('odt-secrets');
@@ -7934,7 +8032,10 @@ window.odtSendChatMessage = async () => {
7934
8032
  const _sendQ = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
7935
8033
  const res = await fetch(`/api/orgs/${encodeURIComponent(_v2SelOrg)}/chat${_sendQ}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text, role: role || undefined, runId: _odtChatCurrentId || null }) });
7936
8034
  const data = await res.json().catch(() => ({}));
7937
- if (!data.delivered) {
8035
+ if (!res.ok || !data.ok) {
8036
+ inp.value = text;
8037
+ showToast('Not delivered', data.error || `HTTP ${res.status}`, 'err');
8038
+ } else if (data.delivery !== 'live') {
7938
8039
  const lbl = document.getElementById('odt-chat-live-lbl');
7939
8040
  if (lbl) { const prev = lbl.textContent; lbl.textContent = 'QUEUED (org offline)'; setTimeout(() => { lbl.textContent = prev; }, 4000); }
7940
8041
  }
@@ -8484,8 +8585,16 @@ function _odtHandleLiveEvent(ev) {
8484
8585
  }
8485
8586
  if (ev?.type === 'org:question' && ev.from) {
8486
8587
  if (ev.org === _v2SelOrg) v2ShowQuestionBubble(ev.from, ev.question || '');
8487
- if (viewRendered.humaninput) renderHumanInputView();
8488
- else updateHumanInputBadge((parseInt(document.getElementById('bdg-humaninput')?.textContent, 10) || 0) + 1);
8588
+ }
8589
+ _v2RuntimeNudge(ev);
8590
+ // Anything a human must act on, or just did: refresh the Human Input list,
8591
+ // its badge, and the selected org's approvals/decisions panes.
8592
+ if (['org:question', 'org:gate', 'org:question-answered', 'org:approval:resolved', 'org:gate:resolved'].includes(ev?.type)) {
8593
+ _hiScheduleReload();
8594
+ if (ev.org === _v2SelOrg) {
8595
+ if (document.getElementById('odt-approvals')?.offsetParent) v2RenderOrgApprovals();
8596
+ if (document.getElementById('odt-decisions')?.offsetParent) v2RenderOrgDecisions();
8597
+ }
8489
8598
  }
8490
8599
  if (ev?.type === 'org:question-answered' && ev.role) {
8491
8600
  if (ev.org === _v2SelOrg) v2ClearQuestionBubble(ev.role);
@@ -8596,90 +8705,273 @@ function _odtHandleLiveEvent(ev) {
8596
8705
  }
8597
8706
  }
8598
8707
 
8708
+ // ── RUNTIME ────────────────────────────────────────────────
8709
+ // What the org is doing, from the runtime's own sources (GET /api/org/:n/runtime):
8710
+ // live status, idle watchdog + hold deadline, budget on its enforcement basis,
8711
+ // per-role model/effort/tier and usage (cache tokens included), the task DAG
8712
+ // with evidence-gate failures, gate/watchdog audit events, and how runs ended.
8713
+ let _v2RuntimeTimer = null;
8714
+ let _v2RuntimeReload = null;
8715
+
8716
+ function _rtTok(n) {
8717
+ if (n === null || n === undefined) return '?';
8718
+ n = Number(n) || 0;
8719
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
8720
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
8721
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
8722
+ return String(n);
8723
+ }
8724
+ function _rtClock(iso) {
8725
+ const t = typeof iso === 'number' ? iso : Date.parse(iso || '');
8726
+ return t ? new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—';
8727
+ }
8728
+ function _rtIn(iso) {
8729
+ const ms = (typeof iso === 'number' ? iso : Date.parse(iso || '')) - Date.now();
8730
+ if (!Number.isFinite(ms)) return '';
8731
+ return ms <= 0 ? 'now' : `in ${fmtDur(ms)}`;
8732
+ }
8733
+ const _RT_CLOSED_BY = {
8734
+ 'org-complete': 'boss called org_complete',
8735
+ 'idle-stop': 'idle watchdog stopped it',
8736
+ 'failed-start': 'failed to start',
8737
+ 'scheduled-deadline': 'scheduled deadline',
8738
+ 'boss-restart': 'boss restarted',
8739
+ 'boss-restart-exhausted': 'boss restarts exhausted',
8740
+ 'mark-complete': 'marked complete by hand',
8741
+ };
8742
+ const _RT_AUDIT_COLOR = {
8743
+ 'task-evidence-refused': 'var(--amber, #d29922)',
8744
+ 'task-evidence-escalated': 'var(--red)',
8745
+ 'org-complete-refused': 'var(--amber, #d29922)',
8746
+ 'hold-expired': 'var(--amber, #d29922)',
8747
+ 'no-progress': 'var(--amber, #d29922)',
8748
+ 'idle-nudge': 'var(--text-lo)',
8749
+ 'idle-stop': 'var(--red)',
8750
+ };
8751
+
8752
+ function _rtCard(label, value, sub) {
8753
+ return `<div style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;min-width:0">
8754
+ <div style="font-size:9px;letter-spacing:1.5px;color:var(--text-xs)">${label}</div>
8755
+ <div style="font-size:14px;color:var(--text-hi);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${value}</div>
8756
+ ${sub ? `<div style="font-size:11px;color:var(--text-lo);margin-top:2px">${sub}</div>` : ''}
8757
+ </div>`;
8758
+ }
8759
+ function _rtTable(title, head, rows, empty) {
8760
+ const h = `<div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin:18px 0 6px">${title}</div>`;
8761
+ if (!rows.length) return `${h}<div class="empty" style="padding:8px 0">${empty}</div>`;
8762
+ return `${h}<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
8763
+ <thead><tr style="color:var(--text-xs);text-align:left">${head.map(c => `<th style="padding:4px 8px;font-weight:500;white-space:nowrap">${c}</th>`).join('')}</tr></thead>
8764
+ <tbody>${rows.map(r => `<tr style="border-top:1px solid var(--border)">${r.map(c => `<td style="padding:5px 8px;vertical-align:top">${c}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;
8765
+ }
8766
+
8767
+ async function v2RenderOrgRuntime() {
8768
+ const el = document.getElementById('odt-runtime');
8769
+ if (!el || !_v2SelOrg) return;
8770
+ clearTimeout(_v2RuntimeTimer);
8771
+ const _rendOrg = _v2SelOrg;
8772
+ if (!el.dataset.org || el.dataset.org !== _rendOrg) el.innerHTML = '<div class="loading-txt">Loading…</div>';
8773
+ let v;
8774
+ try {
8775
+ const res = await fetch(`/api/org/${enc(_rendOrg)}/runtime${DIR ? `?dir=${enc(DIR)}` : ''}`);
8776
+ v = await res.json().catch(() => ({}));
8777
+ if (!res.ok) throw new Error(v.error || `HTTP ${res.status}`);
8778
+ } catch (e) {
8779
+ if (_v2SelOrg === _rendOrg) el.innerHTML = `<div class="empty">Failed: ${esc(e.message)}</div>`;
8780
+ return;
8781
+ }
8782
+ if (_v2SelOrg !== _rendOrg || _v2OrgTab !== 'runtime') return;
8783
+ el.dataset.org = _rendOrg;
8784
+ const s = v.settings || {};
8785
+ const mono = t => `<span style="font-family:var(--mono)">${t}</span>`;
8786
+
8787
+ // Status strip
8788
+ const statusSub = v.live
8789
+ ? `${esc(v.run || '')}${v.startedAt ? ` · up ${fmtDur(Date.now() - v.startedAt)}` : ''}`
8790
+ : `${esc(v.run || 'no run yet')}${v.closedBy ? ` · ${esc(_RT_CLOSED_BY[v.closedBy] || v.closedBy)}` : ''}`;
8791
+ let idle = '—', idleSub = s.idle_minutes === 0 ? 'watchdog disabled' : `window ${s.idle_minutes}m`;
8792
+ if (v.idle) {
8793
+ if (v.idle.idle_hold && v.idle.idle_hold !== 'unknown') {
8794
+ idle = `held: ${esc(v.idle.idle_hold)}`;
8795
+ idleSub = v.idle.idle_hold_until ? `hold ends ${_rtClock(v.idle.idle_hold_until)} (${_rtIn(v.idle.idle_hold_until)})` : 'hold has no deadline';
8796
+ } else if (v.idle.idle_stop_at) {
8797
+ idle = `stops ${_rtIn(v.idle.idle_stop_at)}`;
8798
+ idleSub = `if idle until ${_rtClock(v.idle.idle_stop_at)}`;
8799
+ } else idle = 'not reported yet';
8800
+ }
8801
+ const b = v.budget || {};
8802
+ const pct = b.tokens && b.used !== null ? Math.min(100, (b.used / b.tokens) * 100) : null;
8803
+ const budget = b.used === null ? 'unknown' : `${_rtTok(b.used)} / ${_rtTok(b.tokens)}`;
8804
+ const budgetWhy = b.used === null ? ' · run predates cache metering' : '';
8805
+ const budgetSub = `${esc(b.basis)} basis${pct !== null ? ` · ${pct.toFixed(1)}%` : ''}${budgetWhy} · ${fmtCost(v.totals?.cost_usd)}`;
8806
+ const gate = s.completion_evidence ? 'evidence required' : 'off';
8807
+ const gateSub = s.completion_evidence
8808
+ ? `escalate after ${s.max_evidence_attempts} refusals · org_complete: ${esc(s.completion)}`
8809
+ : `org_complete gate: ${esc(s.completion)}`;
8810
+
8811
+ let html = '<div style="padding:16px">';
8812
+ if (!v.valid) html += `<div class="empty" style="color:var(--red);text-align:left;margin-bottom:10px">Definition does not parse — <code>org run</code> will refuse it:<br>${v.problems.map(esc).join('<br>')}</div>`;
8813
+ if (v.liveError) html += `<div class="empty" style="color:var(--amber, #d29922);text-align:left;margin-bottom:10px">A daemon is registered for this org but did not answer: ${esc(v.liveError)}</div>`;
8814
+ html += `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px">
8815
+ ${_rtCard('STATUS', `<span class="ss-pill ${v.live ? 'on' : ''}">${esc(v.status)}</span>`, statusSub)}
8816
+ ${_rtCard('IDLE WATCHDOG', idle, idleSub)}
8817
+ ${_rtCard('BUDGET (THIS RUN)', budget, budgetSub)}
8818
+ ${_rtCard('COMPLETION GATE', gate, gateSub)}
8819
+ ${_rtCard('SESSIONS', `per ${esc(s.session_scope)}`, `workspace: ${esc(s.workspace)} · max ${s.max_concurrent_agents} agents`)}
8820
+ </div>`;
8821
+
8822
+ // Roles
8823
+ html += _rtTable(`ROLES${v.run ? ` — USAGE IN ${esc(v.run)}` : ''}`,
8824
+ ['Role', 'Runtime', 'Model', 'Effort', 'Tier', 'Status', 'In', 'Out', 'Cache read', 'Cache write', 'Cost', 'Last active'],
8825
+ (v.roles || []).map(r => [
8826
+ `<span style="color:var(--text-hi)">${esc(r.title)}</span>${r.title !== r.id ? `<div style="font-size:10px;color:var(--text-xs)">${esc(r.id)}</div>` : ''}`,
8827
+ esc(r.runtime),
8828
+ r.tierError
8829
+ ? `<span style="color:var(--red)" title="${esc(r.tierError)}">tier error</span>`
8830
+ : `<span style="white-space:nowrap">${mono(esc(r.model || 'default'))}</span><div style="font-size:10px;color:var(--text-xs)">${esc(r.modelSource)}</div>`,
8831
+ esc(r.effort || '—'),
8832
+ esc(r.tier || '—'),
8833
+ `<span class="ss-pill ${r.status === 'running' || r.status === 'working' ? 'on' : ''}">${esc(r.status)}</span>`,
8834
+ // A run recorded before the four-way meter only has an input+output total.
8835
+ ...(r.usage.legacy_tokens && !r.usage.tokens
8836
+ ? [`<span title="Recorded before the meter split input, output and cache tokens">${mono(_rtTok(r.usage.legacy_tokens))} in+out</span>`, '—', '—', '—']
8837
+ : [mono(_rtTok(r.usage.tokens_in)), mono(_rtTok(r.usage.tokens_out)),
8838
+ mono(_rtTok(r.usage.cache_read)), mono(_rtTok(r.usage.cache_creation))]),
8839
+ mono(fmtCost(r.usage.cost_usd)),
8840
+ `<span style="color:var(--text-xs)">${r.lastActivity ? relTime(r.lastActivity) : '—'}</span>`,
8841
+ ]), 'No roles');
8842
+
8843
+ // Tasks
8844
+ const open = (v.tasks || []).filter(t => !['done', 'cancelled', 'merged', 'split'].includes(t.status));
8845
+ html += _rtTable(`TASKS (${open.length} open of ${(v.tasks || []).length})`,
8846
+ ['Task', 'Assignee', 'Status', 'Evidence refusals', 'Loadout', 'Blocked'],
8847
+ [...open, ...(v.tasks || []).filter(t => !open.includes(t))].slice(0, 60).map(t => [
8848
+ `<span style="color:var(--text-hi)">${esc(t.title || t.id)}</span><div style="font-size:10px;color:var(--text-xs)">${esc(t.id)}</div>`,
8849
+ esc(t.assignee || '—'),
8850
+ `<span class="ss-pill ${t.status === 'done' ? 'on' : t.status === 'failed' || t.status === 'blocked' ? 'warn' : ''}">${esc(t.status)}</span>`,
8851
+ t.evidenceFailures ? `<span style="color:${t.evidenceFailures >= s.max_evidence_attempts ? 'var(--red)' : 'var(--amber, #d29922)'}">${t.evidenceFailures} / ${s.max_evidence_attempts}</span>` : '—',
8852
+ esc(t.loadout || '—'),
8853
+ t.blockedUntil ? `until ${_rtClock(t.blockedUntil)}${t.blockedReason ? ` — ${esc(t.blockedReason)}` : ''}` : '—',
8854
+ ]), 'No task graph for this run (the boss has not used org_task)');
8855
+
8856
+ // Audit trail
8857
+ html += _rtTable('EVIDENCE GATE & WATCHDOG EVENTS', ['When', 'Event', 'Role', 'Detail'],
8858
+ (v.audit || []).map(a => [
8859
+ `<span style="color:var(--text-xs);white-space:nowrap">${_rtClock(a.ts)}</span>`,
8860
+ `<span class="ss-pill" style="color:${_RT_AUDIT_COLOR[a.reason] || 'var(--text-lo)'};border-color:currentColor;white-space:nowrap">${esc(a.reason)}</span>`,
8861
+ esc(a.from || '—'),
8862
+ `<div style="white-space:pre-wrap;max-height:120px;overflow:auto">${esc(a.msg)}${a.data?.refusal ? `\n<span style="color:var(--text-lo)">${esc(a.data.refusal)}</span>` : ''}</div>`,
8863
+ ]), 'None in this run');
8864
+
8865
+ // History
8866
+ html += _rtTable('RECENT RUNS', ['Run', 'Ended', 'Duration', 'How it ended', 'Outcome', 'Open tasks at stop', 'Tokens', 'Cost'],
8867
+ (v.history || []).map(h => [
8868
+ mono(esc(h.run)),
8869
+ `<span style="color:var(--text-xs)">${relTime(h.endedAt)}</span>`,
8870
+ h.durationMs ? fmtDur(h.durationMs) : '—',
8871
+ esc(h.closedBy ? (_RT_CLOSED_BY[h.closedBy] || h.closedBy) : 'stopped (manual or shutdown)') + (h.crashes?.length ? `<div style="color:var(--red);font-size:10px">crashed: ${h.crashes.map(esc).join(', ')}</div>` : ''),
8872
+ h.outcome
8873
+ ? `<span class="ss-pill ${h.outcome.status === 'achieved' ? 'on' : 'warn'}">${esc(h.outcome.status)}</span>${h.blocker ? ` blocker: ${esc(h.blocker)}` : ''}<div style="font-size:11px;color:var(--text-lo);max-width:320px;white-space:pre-wrap;max-height:80px;overflow:auto">${esc(h.blockerDetail || h.outcome.summary || '')}</div>`
8874
+ : '<span style="color:var(--text-xs)">none claimed</span>',
8875
+ h.runnableTasksAtStop ? `<span style="color:var(--amber, #d29922)">${h.runnableTasksAtStop}</span>` : '0',
8876
+ mono(_rtTok(h.totalTokens)),
8877
+ mono(fmtCost(h.totalCostUsd)),
8878
+ ]), 'No finished runs yet');
8879
+ html += '</div>';
8880
+ el.innerHTML = html;
8881
+ // A live run changes by the second; poll lightly on top of the SSE nudges.
8882
+ if (v.live) _v2RuntimeTimer = setTimeout(() => { if (_v2OrgTab === 'runtime') v2RenderOrgRuntime(); }, 15000);
8883
+ }
8884
+
8885
+ /** SSE nudge: refresh the Runtime pane shortly after an event for its org. */
8886
+ function _v2RuntimeNudge(ev) {
8887
+ if (_v2OrgTab !== 'runtime' || !ev?.org || ev.org !== _v2SelOrg) return;
8888
+ clearTimeout(_v2RuntimeReload);
8889
+ _v2RuntimeReload = setTimeout(v2RenderOrgRuntime, 2000);
8890
+ }
8891
+
8599
8892
  // ── ORG CONFIG ─────────────────────────────────────────────
8893
+ // Edits the Org Runtime v2 definition (goal, schedule, run_config, default
8894
+ // cost tier). Saved through POST /api/org/:n/config, which merges into the file
8895
+ // on disk and refuses anything `org run` would refuse. Roles, policies and
8896
+ // providers stay in the JSON (Files tab / `monomind org edit`).
8600
8897
  function v2RenderOrgConfig() {
8601
8898
  const el = document.getElementById('odt-config');
8602
8899
  if (!el || !_v2OrgData || !_v2SelOrg) return;
8603
- const d = _v2OrgData;
8900
+ const d = _v2OrgData.config || _v2OrgData;
8604
8901
  const rc = d.run_config || {};
8605
- const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
8606
- const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
8607
- const modes = [{ v: 'daemon', l: 'persistent (daemon)' }, { v: 'once', l: 'once' }, { v: 'scheduled', l: 'scheduled' }];
8608
- const statuses = ['active','paused','archived'];
8609
- const govPolicies = ['auto','board','strict'];
8610
- const inp = (id,val,type,extra) => `<input class="filter-input" id="${id}" type="${type||'text'}" value="${esc(String(val??''))}" ${extra||''} style="width:100%;box-sizing:border-box">`;
8611
- const sel = (id,opts,cur) => `<select class="filter-input" id="${id}" style="width:100%;box-sizing:border-box;cursor:pointer">${opts.map(o=>`<option${cur===o?' selected':''}>${esc(o)}</option>`).join('')}</select>`;
8612
- const fld = (lbl,html) => `<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:10px;color:var(--text-lo)">${lbl}</div>${html}</div>`;
8613
- const sec = (title,inner) => `<div style="margin-bottom:22px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid var(--border)">${title}</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:11px">${inner}</div></div>`;
8614
- el.innerHTML = '<div style="padding:18px;overflow-y:auto;max-width:660px">'
8615
- +sec('GENERAL',
8616
- fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
8617
- +fld('Status', sel('oc-status', statuses, d.status||'active'))
8618
- +'<div style="grid-column:1/-1">'+fld('Goal', `<textarea id="oc-goal" class="filter-input" rows="3" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">${esc(d.goal||'')}</textarea>`)+'</div>'
8619
- +fld('Mode', `<select class="filter-input" id="oc-mode" style="width:100%;box-sizing:border-box;cursor:pointer">${modes.map(o=>`<option value="${esc(o.v)}"${(d.mode||'daemon')===o.v?' selected':''}>${esc(o.l)}</option>`).join('')}</select>`)
8620
- +fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
8621
- +'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
8622
- )
8623
- +sec('RUN CONFIG',
8624
- fld('Max Concurrent Agents', inp('oc-max-agents', rc.max_concurrent_agents??6,'number','min="1" max="32"'))
8625
- +fld('Checkpoint Interval (min)', inp('oc-checkpoint', rc.checkpoint_interval_min??30,'number','min="5"'))
8626
- +fld('Budget Tokens (0=unlimited)', inp('oc-budget', rc.budget_tokens??0,'number','min="0"'))
8627
- +fld('Alert Threshold (0-1)', inp('oc-alert', rc.alert_threshold??0.8,'number','step="0.05" min="0" max="1"'))
8628
- +fld('Boss Role ID', inp('oc-boss', rc.boss_role||''))
8629
- +fld('Memory Namespace', inp('oc-namespace', rc.memory_namespace||(`org:${d.name}`)))
8630
- +'<div style="grid-column:1/-1;display:flex;align-items:center;gap:8px;margin-top:4px"><input type="checkbox" id="oc-spawn-all" '+(rc.spawn_all_roles?'checked':'')+' style="accent-color:var(--accent);width:14px;height:14px;cursor:pointer"><label for="oc-spawn-all" style="font-size:12px;color:var(--text-mid);cursor:pointer">Spawn all roles as separate agents</label></div>'
8631
- )
8632
- +sec('GOVERNANCE',
8633
- fld('Approval Policy', sel('oc-gov-policy', govPolicies, gov.policy||'auto'))
8634
- +fld('Approvals File', inp('oc-gov-file', gov.approvals_file||(`.monomind/orgs/${d.name}-approvals.json`)))
8635
- )
8636
- +'<div style="display:flex;align-items:center;gap:10px"><button class="btn" style="border-color:var(--accent);color:var(--accent)" onclick="v2SaveOrgConfig()">Save Config</button><span id="oc-status-msg" style="font-size:11px;color:var(--text-lo)"></span></div>'
8637
- +'</div>';
8902
+ const inp = (id, val, type, extra) => `<input class="filter-input" id="${id}" type="${type || 'text'}" value="${esc(String(val ?? ''))}" ${extra || ''} style="width:100%;box-sizing:border-box">`;
8903
+ const sel = (id, opts, cur) => `<select class="filter-input" id="${id}" style="width:100%;box-sizing:border-box;cursor:pointer">${opts.map(([v, l]) => `<option value="${esc(v)}"${String(cur ?? '') === v ? ' selected' : ''}>${esc(l)}</option>`).join('')}</select>`;
8904
+ const fld = (lbl, html, hint) => `<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:10px;color:var(--text-lo)">${lbl}</div>${html}${hint ? `<div style="font-size:10px;color:var(--text-xs)">${hint}</div>` : ''}</div>`;
8905
+ const sec = (title, inner) => `<div style="margin-bottom:22px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid var(--border)">${title}</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:11px">${inner}</div></div>`;
8906
+ const tiers = ['standard', 'economy', 'budget', ...Object.keys(d.cost_tiers?.tiers || {})];
8907
+ el.innerHTML = '<div style="padding:18px;overflow-y:auto;max-width:720px">'
8908
+ + sec('GENERAL',
8909
+ fld('Name', inp('oc-name', d.name || '', 'text', 'readonly style="opacity:0.5"'))
8910
+ + fld('Schedule', inp('oc-schedule', d.schedule ?? '', 'text', 'placeholder="cron, or empty for none"'))
8911
+ + `<div style="grid-column:1/-1">${fld('Goal', `<textarea id="oc-goal" class="filter-input" rows="4" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">${esc(d.goal || '')}</textarea>`)}</div>`)
8912
+ + sec('BUDGET & CONCURRENCY',
8913
+ fld('Budget tokens (per run)', inp('oc-budget', rc.budget_tokens ?? '', 'number', 'min="1" placeholder="1000000"'), 'Must be positive.')
8914
+ + fld('Budget basis', sel('oc-basis', [['', 'uncached (default)'], ['uncached', 'uncached — input + output'], ['billable', 'billable — includes cache tokens']], rc.budget_tokens_basis ?? ''), 'billable is ~100x larger on a cached run; resize the budget with it.')
8915
+ + fld('Max concurrent agents', inp('oc-max-agents', rc.max_concurrent_agents ?? '', 'number', 'min="1" placeholder="4"'))
8916
+ + fld('Max turns per message', inp('oc-max-turns', rc.max_turns_per_message ?? '', 'number', 'min="1"')))
8917
+ + sec('COMPLETION',
8918
+ fld('org_complete gate', sel('oc-completion', [['', 'boss (default)'], ['boss', 'boss — partial needs a real blocker'], ['dag', 'dag — also refuse while tasks are runnable']], rc.completion ?? ''))
8919
+ + fld('Evidence gate on org_task_done', sel('oc-evidence', [['false', 'off'], ['true', 'on — commands + exit codes pinned to HEAD']], String(!!rc.completion_evidence)))
8920
+ + fld('Escalate after N evidence refusals', inp('oc-evidence-max', rc.max_evidence_attempts ?? '', 'number', 'min="1" placeholder="3"'), 'Then the task is failed and handed to the boss.'))
8921
+ + sec('SESSIONS & WATCHDOG',
8922
+ fld('Idle minutes (0 disables)', inp('oc-idle', rc.idle_minutes ?? '', 'number', 'min="0" step="any" placeholder="10"'), 'Every hold on the idle clock expires after an hour.')
8923
+ + fld('Session scope', sel('oc-scope', [['', 'role (default)'], ['role', 'role — one session per role'], ['task', 'task — one session per task']], rc.session_scope ?? ''))
8924
+ + fld('Workspace', inp('oc-workspace', rc.workspace ?? '', 'text', 'placeholder="repo | isolated | worktree | worktree-per-role | /abs/path"'))
8925
+ + fld('Memory namespace', inp('oc-namespace', rc.memory_namespace ?? '', 'text', `placeholder="org:${esc(d.name || '')}"`)))
8926
+ + sec('COST TIERS',
8927
+ fld('Default tier', sel('oc-tier', [['', 'none — models as configured'], ...[...new Set(tiers)].map(t => [t, t])], d.cost_tiers?.default ?? ''), 'Per-role tiers and exemptions: edit cost_tiers.roles in the JSON.'))
8928
+ + '<div style="display:flex;align-items:flex-start;gap:10px"><button class="btn" style="border-color:var(--accent);color:var(--accent)" onclick="v2SaveOrgConfig()">Save Config</button><span id="oc-status-msg" style="font-size:11px;color:var(--text-lo);white-space:pre-wrap"></span></div>'
8929
+ + '</div>';
8638
8930
  }
8639
8931
 
8640
8932
  async function _v2SaveOrgConfig() {
8641
8933
  if (!_v2SelOrg || !_v2OrgData) return;
8642
8934
  const g = id => document.getElementById(id);
8643
- // Only include org config fields — exclude runtime data (state, goals, routines, approvals,
8644
- // running, tasks, config nested copy) that would corrupt the org .json file.
8645
- const _RUNTIME_KEYS = new Set(['config','state','goals','routines','approvals','running','tasks']);
8646
- const _baseConfig = Object.fromEntries(
8647
- Object.entries(_v2OrgData).filter(([k]) => !k.startsWith('_') && !_RUNTIME_KEYS.has(k))
8648
- );
8649
- const cfg = {
8650
- ..._baseConfig,
8651
- status: (g('oc-status')?.value) || _v2OrgData.status,
8652
- goal: (g('oc-goal')?.value) || _v2OrgData.goal,
8653
- mode: (g('oc-mode')?.value) || _v2OrgData.mode,
8654
- topology: (g('oc-topology')?.value) || _v2OrgData.topology,
8655
- schedule: (g('oc-schedule')?.value||'').trim() || null,
8656
- run_config: { ...(_v2OrgData.run_config||{}),
8657
- max_concurrent_agents: parseInt(g('oc-max-agents')?.value, 10)||6,
8658
- checkpoint_interval_min: parseInt(g('oc-checkpoint')?.value, 10)||30,
8659
- budget_tokens: parseInt(g('oc-budget')?.value, 10)||0,
8660
- alert_threshold: parseFloat(g('oc-alert')?.value)||0.8,
8661
- boss_role: (g('oc-boss')?.value||'').trim(),
8662
- memory_namespace: (g('oc-namespace')?.value||'').trim()||(`org:${_v2SelOrg}`),
8663
- spawn_all_roles: !!(g('oc-spawn-all')?.checked),
8664
- },
8665
- governance: {
8666
- policy: (g('oc-gov-policy')?.value)||'auto',
8667
- approvals_file: (g('oc-gov-file')?.value||'').trim()||(`.monomind/orgs/${_v2SelOrg}-approvals.json`),
8935
+ // '' = not set → null removes the key so the runtime default applies.
8936
+ const num = id => { const v = (g(id)?.value || '').trim(); return v === '' ? null : Number(v); };
8937
+ const str = id => { const v = (g(id)?.value || '').trim(); return v === '' ? null : v; };
8938
+ const patch = {
8939
+ goal: g('oc-goal')?.value ?? '',
8940
+ // A numeric schedule (an interval) stays a number; a cron stays a string.
8941
+ schedule: (() => {
8942
+ const v = str('oc-schedule');
8943
+ const was = (_v2OrgData.config || _v2OrgData).schedule;
8944
+ return v !== null && typeof was === 'number' && /^\d+$/.test(v) ? Number(v) : v;
8945
+ })(),
8946
+ run_config: {
8947
+ budget_tokens: num('oc-budget'),
8948
+ budget_tokens_basis: str('oc-basis'),
8949
+ max_concurrent_agents: num('oc-max-agents'),
8950
+ max_turns_per_message: num('oc-max-turns'),
8951
+ completion: str('oc-completion'),
8952
+ completion_evidence: g('oc-evidence')?.value === 'true' ? true : null,
8953
+ max_evidence_attempts: num('oc-evidence-max'),
8954
+ idle_minutes: num('oc-idle'),
8955
+ session_scope: str('oc-scope'),
8956
+ workspace: str('oc-workspace'),
8957
+ memory_namespace: str('oc-namespace'),
8668
8958
  },
8959
+ cost_tiers_default: str('oc-tier') || '',
8669
8960
  };
8670
- const msg = document.getElementById('oc-status-msg');
8671
- if (msg) { msg.style.color='var(--text-lo)'; msg.textContent='Saving...'; }
8961
+ const msg = g('oc-status-msg');
8962
+ if (msg) { msg.style.color = 'var(--text-lo)'; msg.textContent = 'Saving…'; }
8672
8963
  try {
8673
- const qs = (typeof DIR !== 'undefined' && DIR) ? `?dir=${encodeURIComponent(DIR)}` : '';
8674
- const r = await fetch(`/api/orgs${qs}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg) });
8675
- const json = await r.json();
8676
- if (json.ok) {
8677
- _v2OrgData = Object.assign({}, _v2OrgData, cfg);
8678
- if (msg) { msg.style.color='oklch(68% 0.20 150)'; msg.textContent='Saved'; setTimeout(()=>{ if(msg) msg.textContent=''; }, 2500); }
8679
- if (typeof showToast === 'function') showToast('Config saved','','ok');
8680
- } else { throw new Error(json.error||'Save failed'); }
8681
- } catch(e) {
8682
- if (msg) { msg.style.color='oklch(60% 0.22 25)'; msg.textContent=`Error: ${e.message}`; }
8964
+ const r = await fetch(`/api/org/${enc(_v2SelOrg)}/config${DIR ? `?dir=${enc(DIR)}` : ''}`, {
8965
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch),
8966
+ });
8967
+ const json = await r.json().catch(() => ({}));
8968
+ if (!r.ok || !json.ok) throw new Error((json.error || `HTTP ${r.status}`).split('; ').join('\n'));
8969
+ if (_v2OrgData.config) _v2OrgData.config = json.def;
8970
+ Object.assign(_v2OrgData, { goal: json.def.goal, schedule: json.def.schedule, run_config: json.def.run_config, cost_tiers: json.def.cost_tiers });
8971
+ if (msg) { msg.style.color = 'oklch(68% 0.20 150)'; msg.textContent = 'Saved — applies from the next run'; setTimeout(() => { if (msg) msg.textContent = ''; }, 4000); }
8972
+ showToast('Config saved', 'Validated against the org schema', 'ok');
8973
+ } catch (e) {
8974
+ if (msg) { msg.style.color = 'oklch(60% 0.22 25)'; msg.textContent = `Not saved:\n${e.message}`; }
8683
8975
  }
8684
8976
  }
8685
8977
 
@@ -14211,7 +14503,7 @@ Object.assign(window, {
14211
14503
  gdOpen: _gdOpen,
14212
14504
  gdRevealPath: _gdRevealPath,
14213
14505
  gdCopyPath: _gdCopyPath,
14214
- submitHumanAnswer: _submitHumanAnswer,
14506
+ hiAct: _hiAct,
14215
14507
  v2ViewOrgFile: _v2ViewOrgFile,
14216
14508
  minimapJump: _minimapJump,
14217
14509
  stopLoop: _stopLoop,
@@ -14220,6 +14512,7 @@ Object.assign(window, {
14220
14512
  v2TriggerAvatarUpload: _v2TriggerAvatarUpload,
14221
14513
  v2HandleAvatarFile: _v2HandleAvatarFile,
14222
14514
  orgApprovalAction: _orgApprovalAction,
14515
+ orgGateAction: _orgGateAction,
14223
14516
  generateOrgSettingsCmd: _generateOrgSettingsCmd,
14224
14517
  setOdtChatMode: _setOdtChatMode,
14225
14518
  v2SaveOrgConfig: _v2SaveOrgConfig,