@monoes/monomindcli 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  2. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  3. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  4. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  5. package/dist/src/commands/doctor-project-checks.d.ts +12 -1
  6. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  7. package/dist/src/commands/doctor-project-checks.js +55 -4
  8. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  9. package/dist/src/commands/doctor.d.ts.map +1 -1
  10. package/dist/src/commands/doctor.js +10 -1
  11. package/dist/src/commands/doctor.js.map +1 -1
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/init.js +17 -0
  14. package/dist/src/commands/init.js.map +1 -1
  15. package/dist/src/commands/mcp.d.ts.map +1 -1
  16. package/dist/src/commands/mcp.js +4 -3
  17. package/dist/src/commands/mcp.js.map +1 -1
  18. package/dist/src/commands/org-observe.d.ts.map +1 -1
  19. package/dist/src/commands/org-observe.js +14 -0
  20. package/dist/src/commands/org-observe.js.map +1 -1
  21. package/dist/src/commands/org.d.ts.map +1 -1
  22. package/dist/src/commands/org.js +12 -2
  23. package/dist/src/commands/org.js.map +1 -1
  24. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  25. package/dist/src/init/claudemd-generator.js +2 -1
  26. package/dist/src/init/claudemd-generator.js.map +1 -1
  27. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  28. package/dist/src/init/mcp-generator.js +3 -3
  29. package/dist/src/init/mcp-generator.js.map +1 -1
  30. package/dist/src/init/types.d.ts +6 -0
  31. package/dist/src/init/types.d.ts.map +1 -1
  32. package/dist/src/init/types.js.map +1 -1
  33. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  34. package/dist/src/init/write-runtime-config.js +12 -2
  35. package/dist/src/init/write-runtime-config.js.map +1 -1
  36. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  37. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  38. package/dist/src/orgrt/agent-runner.js +47 -0
  39. package/dist/src/orgrt/agent-runner.js.map +1 -1
  40. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  41. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  42. package/dist/src/orgrt/checkpoint.js +15 -6
  43. package/dist/src/orgrt/checkpoint.js.map +1 -1
  44. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  45. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  46. package/dist/src/orgrt/completion-gate.js +45 -0
  47. package/dist/src/orgrt/completion-gate.js.map +1 -1
  48. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  49. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  50. package/dist/src/orgrt/cost-tier.js +183 -0
  51. package/dist/src/orgrt/cost-tier.js.map +1 -0
  52. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  53. package/dist/src/orgrt/cross-org.js +14 -0
  54. package/dist/src/orgrt/cross-org.js.map +1 -1
  55. package/dist/src/orgrt/daemon.d.ts +12 -1
  56. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  57. package/dist/src/orgrt/daemon.js +216 -49
  58. package/dist/src/orgrt/daemon.js.map +1 -1
  59. package/dist/src/orgrt/decisions.d.ts +14 -2
  60. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  61. package/dist/src/orgrt/decisions.js +216 -10
  62. package/dist/src/orgrt/decisions.js.map +1 -1
  63. package/dist/src/orgrt/file-roots.d.ts +7 -0
  64. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  65. package/dist/src/orgrt/file-roots.js +38 -1
  66. package/dist/src/orgrt/file-roots.js.map +1 -1
  67. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  68. package/dist/src/orgrt/forwarder.js +16 -2
  69. package/dist/src/orgrt/forwarder.js.map +1 -1
  70. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  71. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  72. package/dist/src/orgrt/idle-deadline.js +52 -2
  73. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  74. package/dist/src/orgrt/loadouts.d.ts +38 -0
  75. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  76. package/dist/src/orgrt/loadouts.js +129 -0
  77. package/dist/src/orgrt/loadouts.js.map +1 -0
  78. package/dist/src/orgrt/mailbox.d.ts +17 -1
  79. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  80. package/dist/src/orgrt/mailbox.js +50 -3
  81. package/dist/src/orgrt/mailbox.js.map +1 -1
  82. package/dist/src/orgrt/policy.d.ts +42 -2
  83. package/dist/src/orgrt/policy.d.ts.map +1 -1
  84. package/dist/src/orgrt/policy.js +53 -8
  85. package/dist/src/orgrt/policy.js.map +1 -1
  86. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  87. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  88. package/dist/src/orgrt/prompt-vars.js +49 -0
  89. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  90. package/dist/src/orgrt/questions.d.ts +22 -1
  91. package/dist/src/orgrt/questions.d.ts.map +1 -1
  92. package/dist/src/orgrt/questions.js +24 -3
  93. package/dist/src/orgrt/questions.js.map +1 -1
  94. package/dist/src/orgrt/review-packet.d.ts +29 -0
  95. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  96. package/dist/src/orgrt/review-packet.js +67 -0
  97. package/dist/src/orgrt/review-packet.js.map +1 -0
  98. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  99. package/dist/src/orgrt/role-sandbox.js +16 -3
  100. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  101. package/dist/src/orgrt/session-ledger.d.ts +61 -0
  102. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  103. package/dist/src/orgrt/session-ledger.js +113 -0
  104. package/dist/src/orgrt/session-ledger.js.map +1 -0
  105. package/dist/src/orgrt/session.d.ts +45 -5
  106. package/dist/src/orgrt/session.d.ts.map +1 -1
  107. package/dist/src/orgrt/session.js +384 -27
  108. package/dist/src/orgrt/session.js.map +1 -1
  109. package/dist/src/orgrt/task-dag.d.ts +34 -1
  110. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  111. package/dist/src/orgrt/task-dag.js +58 -7
  112. package/dist/src/orgrt/task-dag.js.map +1 -1
  113. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  114. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  115. package/dist/src/orgrt/tool-spill.js +180 -0
  116. package/dist/src/orgrt/tool-spill.js.map +1 -0
  117. package/dist/src/orgrt/types.d.ts +138 -0
  118. package/dist/src/orgrt/types.d.ts.map +1 -1
  119. package/dist/src/orgrt/types.js +148 -0
  120. package/dist/src/orgrt/types.js.map +1 -1
  121. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  122. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  123. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  124. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  125. package/dist/src/protocol-capabilities.d.ts +2 -1
  126. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  127. package/dist/src/protocol-capabilities.js +2 -1
  128. package/dist/src/protocol-capabilities.js.map +1 -1
  129. package/dist/src/ui/dashboard.html +450 -164
  130. package/dist/src/ui/org-hil.mjs +276 -0
  131. package/dist/src/ui/org-runtime.mjs +350 -0
  132. package/dist/src/ui/orgs.html +15 -4
  133. package/dist/src/ui/routes-org.mjs +290 -541
  134. package/dist/src/ui/server.mjs +25 -5
  135. package/dist/tsconfig.tsbuildinfo +1 -1
  136. package/package.json +6 -5
@@ -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>
@@ -2627,6 +2629,8 @@ async function init() {
2627
2629
  sbProjEl.title = DIR;
2628
2630
  _showNavProjectCtx(DIR);
2629
2631
  } catch (_) {}
2632
+ // Count what is waiting on a human now, not only once the view is opened.
2633
+ _hiScheduleReload();
2630
2634
  // deep-link: ?sess=<id>&proj=<path>
2631
2635
  const params = new URLSearchParams(window.location.search);
2632
2636
  let projParam = params.get('proj');
@@ -4446,38 +4450,57 @@ async function _gdCopyPath(p) {
4446
4450
  // renderDocMarkdown() and gdCopyCode() moved to markdown.js (#124) — loaded via
4447
4451
  // <script src="markdown.js"> above; both remain plain globals.
4448
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 }.
4449
4456
  let _hiEntries = [];
4450
4457
  let _hiTab = 'pending';
4451
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
+
4452
4468
  async function renderHumanInputView() {
4453
4469
  const el = document.getElementById('hi-content');
4454
- el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
4470
+ if (!_hiEntries.length) el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
4455
4471
  try {
4456
4472
  const data = await apiFetch('/api/projects');
4457
4473
  const projects = (data?.projects || []).slice(0, 8);
4458
- if (!projects.length) {
4459
- el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No projects found</div></div>';
4460
- _hiEntries = [];
4461
- updateHumanInputBadge(0);
4462
- renderHiCurrentTab(projects.length);
4463
- return;
4464
- }
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) });
4465
4477
  const results = await Promise.allSettled(
4466
- 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 || {} })))
4467
4479
  );
4468
4480
  const entries = [];
4481
+ const errors = [];
4469
4482
  for (const r of results) {
4470
4483
  if (r.status !== 'fulfilled') continue;
4471
- const { project, questions } = r.value;
4472
- 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}`);
4473
4489
  }
4474
- 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));
4475
4491
  _hiEntries = entries;
4476
- updateHumanInputBadge(entries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
4477
- renderHiCurrentTab(projects.length);
4492
+ updateHumanInputBadge(entries.filter(_hiIsPending).length);
4493
+ renderHiCurrentTab(projects.length, errors);
4478
4494
  } catch (e) { el.innerHTML = `<div class="empty">Failed: ${esc(e.message)}</div>`; }
4479
4495
  }
4480
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
+
4481
4504
  function _hiSwitchTab(tab) {
4482
4505
  _hiTab = tab;
4483
4506
  document.querySelectorAll('.hi-tab-bar .odt-btn').forEach(b =>
@@ -4485,39 +4508,80 @@ function _hiSwitchTab(tab) {
4485
4508
  renderHiCurrentTab();
4486
4509
  }
4487
4510
 
4488
- 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) {
4489
4567
  const el = document.getElementById('hi-content');
4490
- const pending = _hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined);
4491
- 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));
4492
4570
  document.getElementById('hi-tab-count-pending').textContent = pending.length || '—';
4493
- document.getElementById('hi-tab-count-answered').textContent = answered.length || '—';
4494
- const entries = _hiTab === 'answered' ? answered : pending;
4571
+ document.getElementById('hi-tab-count-answered').textContent = done.length || '—';
4572
+ const entries = _hiTab === 'answered' ? done : pending;
4495
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;
4496
4575
  document.getElementById('hi-sub').textContent = _hiTab === 'answered'
4497
- ? (answered.length ? `${answered.length} answered question(s)${projTxt}` : `No answered questions${projTxt}`)
4498
- : (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>` : '';
4499
4580
  if (!entries.length) {
4500
- 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>`;
4501
4582
  return;
4502
4583
  }
4503
- el.innerHTML = `<div class="sess-list">${entries.map(({ project, q }) => `
4504
- <div class="sess-row" data-question-id="${esc(q.questionId)}" style="flex-direction:column;align-items:stretch;gap:6px">
4505
- <div style="display:flex;justify-content:space-between;gap:8px">
4506
- <div><b>${esc(q.org)}</b> · ${esc(q.role)} <span style="color:var(--text-xs)">(${esc(project.name || project.path)})</span></div>
4507
- <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(q.ts)}</div>
4508
- </div>
4509
- <div style="color:var(--text-hi)">${esc(q.question)}</div>
4510
- ${_hiTab === 'answered' ? `
4511
- <div style="display:flex;justify-content:space-between;gap:8px;padding:6px 8px;background:var(--surface-hi);border-radius:4px">
4512
- <div><span style="color:var(--text-xs)">Answer:</span> ${esc(q.answer)}</div>
4513
- <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono);flex-shrink:0">${relTime(q.answeredAt)}</div>
4514
- </div>` : `
4515
- <div style="display:flex;gap:6px">
4516
- <input type="text" class="hi-answer-input" placeholder="Your answer…" style="flex:1;padding:4px 8px;font-size:12px" />
4517
- <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>
4518
- </div>`}
4519
- </div>
4520
- `).join('')}</div>`;
4584
+ el.innerHTML = `${errHtml}<div class="sess-list">${entries.map(_hiEntryHtml).join('')}</div>`;
4521
4585
  }
4522
4586
 
4523
4587
  function updateHumanInputBadge(count) {
@@ -4525,26 +4589,38 @@ function updateHumanInputBadge(count) {
4525
4589
  if (b) b.textContent = String(count);
4526
4590
  }
4527
4591
 
4528
- async function _submitHumanAnswer(dir, org, role, questionId, btnEl) {
4529
- const row = btnEl.closest('[data-question-id]');
4530
- const input = row.querySelector('.hi-answer-input');
4531
- const answer = input.value.trim();
4532
- if (!answer) return;
4533
- 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) {
4534
4594
  try {
4535
- const res = await fetch('/api/questions/answer', {
4536
- method: 'POST', headers: { 'Content-Type': 'application/json' },
4537
- 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),
4538
4597
  });
4539
- const data = await res.json();
4540
- if (data.ok) {
4541
- showToast('Answered', `${org}:${role}`, 'ok');
4542
- const entry = _hiEntries.find(({ q }) => q.questionId === questionId);
4543
- if (entry) { entry.q.answer = answer; entry.q.answeredAt = Date.now(); }
4544
- updateHumanInputBadge(_hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
4545
- renderHiCurrentTab();
4546
- } else { showToast('Error', data.error || 'failed', 'err'); btnEl.disabled = false; }
4547
- } 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();
4548
4624
  }
4549
4625
 
4550
4626
  function _cvExcerptText(ev) {
@@ -5909,6 +5985,7 @@ function v2RenderOrgTab(tab) {
5909
5985
  return;
5910
5986
  }
5911
5987
  if (tab === 'chart') v2RenderOrgChart();
5988
+ else if (tab === 'runtime') v2RenderOrgRuntime();
5912
5989
  else if (tab === 'roles') v2RenderOrgRoles();
5913
5990
  else if (tab === 'health') v2RenderOrgHealth();
5914
5991
  else if (tab === 'heartbeats') v2RenderOrgHeartbeats();
@@ -7121,34 +7198,38 @@ function v2RenderOrgLive() {
7121
7198
  }
7122
7199
 
7123
7200
  // ── APPROVALS ──────────────────────────────────────────────
7201
+ // Tool/action approvals (<org>/approvals.json): a role asked to run a
7202
+ // sensitive tool (Bash, WebFetch, org_complete, policy.approvalTools…).
7124
7203
  async function v2RenderOrgApprovals() {
7125
7204
  const el = document.getElementById('odt-approvals');
7126
7205
  if (!el || !_v2SelOrg) return;
7127
7206
  const _rendOrg = _v2SelOrg;
7128
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
7207
+ if (!el.querySelector('table')) el.innerHTML = '<div class="loading-txt">Loading…</div>';
7129
7208
  try {
7130
7209
  const _enc = encodeURIComponent(_rendOrg);
7131
- 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(() => ({}));
7132
7212
  if (_v2SelOrg !== _rendOrg) return;
7133
- const approvals = Array.isArray(data) ? data : (data.approvals || []);
7134
- 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; }
7135
7216
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
7136
7217
  <thead><tr style="color:var(--text-xs);text-align:left">
7137
- <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>
7138
7219
  </tr></thead><tbody>` +
7139
- approvals.slice(0, 50).map(a => {
7140
- const cls = a.status === 'approved' ? 'on' : a.status === 'rejected' ? 'warn' : '';
7141
- const pending = !a.status || a.status === 'pending';
7142
- 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) : '';
7143
7223
  return `<tr style="border-top:1px solid var(--border)">
7144
- <td style="padding:6px 8px;color:var(--text-hi)">${esc(a.requester || a.agent || '—')}</td>
7145
- <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>
7146
- <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status || 'pending')}</span></td>
7147
- <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>
7148
7229
  <td style="padding:6px 8px;white-space:nowrap">
7149
- ${pending
7150
- ? `<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>
7151
- <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>`
7152
7233
  : ''}
7153
7234
  </td>
7154
7235
  </tr>`;
@@ -7157,15 +7238,10 @@ async function v2RenderOrgApprovals() {
7157
7238
  }
7158
7239
 
7159
7240
  async function _orgApprovalAction(id, action) {
7160
- if (!confirm(`${action} this request?`)) return;
7161
- try {
7162
- const _enc = encodeURIComponent(_v2SelOrg);
7163
- await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? `?dir=${encodeURIComponent(DIR)}` : ''}`, {
7164
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
7165
- });
7166
- showToast('Done', `${action}d`, 'ok');
7167
- v2RenderOrgApprovals();
7168
- } 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();
7169
7245
  }
7170
7246
 
7171
7247
  // ── DECISIONS ──────────────────────────────────────────────
@@ -7210,20 +7286,35 @@ function v2RenderDecisionsTable() {
7210
7286
  if (!filtered.length) { el.innerHTML = `${filterBar}<div class="empty">No decision gates match</div>`; return; }
7211
7287
  el.innerHTML = filterBar + `<table style="width:100%;border-collapse:collapse;font-size:12px">
7212
7288
  <thead><tr style="color:var(--text-xs);text-align:left">
7213
- <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>
7214
7290
  </tr></thead><tbody>` +
7215
7291
  filtered.slice(0, 100).map(g => {
7216
7292
  const cls = g.status === 'approved' ? 'on' : g.status === 'rejected' ? 'warn' : '';
7217
7293
  return `<tr style="border-top:1px solid var(--border)">
7218
7294
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(g.roleId || '—')}</td>
7219
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>
7220
- <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(g.status || 'pending')}</span></td>
7221
- <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>
7222
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>
7223
7305
  </tr>`;
7224
7306
  }).join('') + '</tbody></table>';
7225
7307
  }
7226
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
+
7227
7318
  // ── SECRETS ────────────────────────────────────────────────
7228
7319
  async function v2RenderOrgSecrets() {
7229
7320
  const el = document.getElementById('odt-secrets');
@@ -7941,7 +8032,10 @@ window.odtSendChatMessage = async () => {
7941
8032
  const _sendQ = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
7942
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 }) });
7943
8034
  const data = await res.json().catch(() => ({}));
7944
- 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') {
7945
8039
  const lbl = document.getElementById('odt-chat-live-lbl');
7946
8040
  if (lbl) { const prev = lbl.textContent; lbl.textContent = 'QUEUED (org offline)'; setTimeout(() => { lbl.textContent = prev; }, 4000); }
7947
8041
  }
@@ -8491,8 +8585,16 @@ function _odtHandleLiveEvent(ev) {
8491
8585
  }
8492
8586
  if (ev?.type === 'org:question' && ev.from) {
8493
8587
  if (ev.org === _v2SelOrg) v2ShowQuestionBubble(ev.from, ev.question || '');
8494
- if (viewRendered.humaninput) renderHumanInputView();
8495
- 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
+ }
8496
8598
  }
8497
8599
  if (ev?.type === 'org:question-answered' && ev.role) {
8498
8600
  if (ev.org === _v2SelOrg) v2ClearQuestionBubble(ev.role);
@@ -8603,90 +8705,273 @@ function _odtHandleLiveEvent(ev) {
8603
8705
  }
8604
8706
  }
8605
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
+
8606
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`).
8607
8897
  function v2RenderOrgConfig() {
8608
8898
  const el = document.getElementById('odt-config');
8609
8899
  if (!el || !_v2OrgData || !_v2SelOrg) return;
8610
- const d = _v2OrgData;
8900
+ const d = _v2OrgData.config || _v2OrgData;
8611
8901
  const rc = d.run_config || {};
8612
- const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
8613
- const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
8614
- const modes = [{ v: 'daemon', l: 'persistent (daemon)' }, { v: 'once', l: 'once' }, { v: 'scheduled', l: 'scheduled' }];
8615
- const statuses = ['active','paused','archived'];
8616
- const govPolicies = ['auto','board','strict'];
8617
- 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">`;
8618
- 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>`;
8619
- 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>`;
8620
- 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>`;
8621
- el.innerHTML = '<div style="padding:18px;overflow-y:auto;max-width:660px">'
8622
- +sec('GENERAL',
8623
- fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
8624
- +fld('Status', sel('oc-status', statuses, d.status||'active'))
8625
- +'<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>'
8626
- +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>`)
8627
- +fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
8628
- +'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
8629
- )
8630
- +sec('RUN CONFIG',
8631
- fld('Max Concurrent Agents', inp('oc-max-agents', rc.max_concurrent_agents??6,'number','min="1" max="32"'))
8632
- +fld('Checkpoint Interval (min)', inp('oc-checkpoint', rc.checkpoint_interval_min??30,'number','min="5"'))
8633
- +fld('Budget Tokens (0=unlimited)', inp('oc-budget', rc.budget_tokens??0,'number','min="0"'))
8634
- +fld('Alert Threshold (0-1)', inp('oc-alert', rc.alert_threshold??0.8,'number','step="0.05" min="0" max="1"'))
8635
- +fld('Boss Role ID', inp('oc-boss', rc.boss_role||''))
8636
- +fld('Memory Namespace', inp('oc-namespace', rc.memory_namespace||(`org:${d.name}`)))
8637
- +'<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>'
8638
- )
8639
- +sec('GOVERNANCE',
8640
- fld('Approval Policy', sel('oc-gov-policy', govPolicies, gov.policy||'auto'))
8641
- +fld('Approvals File', inp('oc-gov-file', gov.approvals_file||(`.monomind/orgs/${d.name}-approvals.json`)))
8642
- )
8643
- +'<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>'
8644
- +'</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>';
8645
8930
  }
8646
8931
 
8647
8932
  async function _v2SaveOrgConfig() {
8648
8933
  if (!_v2SelOrg || !_v2OrgData) return;
8649
8934
  const g = id => document.getElementById(id);
8650
- // Only include org config fields — exclude runtime data (state, goals, routines, approvals,
8651
- // running, tasks, config nested copy) that would corrupt the org .json file.
8652
- const _RUNTIME_KEYS = new Set(['config','state','goals','routines','approvals','running','tasks']);
8653
- const _baseConfig = Object.fromEntries(
8654
- Object.entries(_v2OrgData).filter(([k]) => !k.startsWith('_') && !_RUNTIME_KEYS.has(k))
8655
- );
8656
- const cfg = {
8657
- ..._baseConfig,
8658
- status: (g('oc-status')?.value) || _v2OrgData.status,
8659
- goal: (g('oc-goal')?.value) || _v2OrgData.goal,
8660
- mode: (g('oc-mode')?.value) || _v2OrgData.mode,
8661
- topology: (g('oc-topology')?.value) || _v2OrgData.topology,
8662
- schedule: (g('oc-schedule')?.value||'').trim() || null,
8663
- run_config: { ...(_v2OrgData.run_config||{}),
8664
- max_concurrent_agents: parseInt(g('oc-max-agents')?.value, 10)||6,
8665
- checkpoint_interval_min: parseInt(g('oc-checkpoint')?.value, 10)||30,
8666
- budget_tokens: parseInt(g('oc-budget')?.value, 10)||0,
8667
- alert_threshold: parseFloat(g('oc-alert')?.value)||0.8,
8668
- boss_role: (g('oc-boss')?.value||'').trim(),
8669
- memory_namespace: (g('oc-namespace')?.value||'').trim()||(`org:${_v2SelOrg}`),
8670
- spawn_all_roles: !!(g('oc-spawn-all')?.checked),
8671
- },
8672
- governance: {
8673
- policy: (g('oc-gov-policy')?.value)||'auto',
8674
- 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'),
8675
8958
  },
8959
+ cost_tiers_default: str('oc-tier') || '',
8676
8960
  };
8677
- const msg = document.getElementById('oc-status-msg');
8678
- 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…'; }
8679
8963
  try {
8680
- const qs = (typeof DIR !== 'undefined' && DIR) ? `?dir=${encodeURIComponent(DIR)}` : '';
8681
- const r = await fetch(`/api/orgs${qs}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg) });
8682
- const json = await r.json();
8683
- if (json.ok) {
8684
- _v2OrgData = Object.assign({}, _v2OrgData, cfg);
8685
- if (msg) { msg.style.color='oklch(68% 0.20 150)'; msg.textContent='Saved'; setTimeout(()=>{ if(msg) msg.textContent=''; }, 2500); }
8686
- if (typeof showToast === 'function') showToast('Config saved','','ok');
8687
- } else { throw new Error(json.error||'Save failed'); }
8688
- } catch(e) {
8689
- 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}`; }
8690
8975
  }
8691
8976
  }
8692
8977
 
@@ -14218,7 +14503,7 @@ Object.assign(window, {
14218
14503
  gdOpen: _gdOpen,
14219
14504
  gdRevealPath: _gdRevealPath,
14220
14505
  gdCopyPath: _gdCopyPath,
14221
- submitHumanAnswer: _submitHumanAnswer,
14506
+ hiAct: _hiAct,
14222
14507
  v2ViewOrgFile: _v2ViewOrgFile,
14223
14508
  minimapJump: _minimapJump,
14224
14509
  stopLoop: _stopLoop,
@@ -14227,6 +14512,7 @@ Object.assign(window, {
14227
14512
  v2TriggerAvatarUpload: _v2TriggerAvatarUpload,
14228
14513
  v2HandleAvatarFile: _v2HandleAvatarFile,
14229
14514
  orgApprovalAction: _orgApprovalAction,
14515
+ orgGateAction: _orgGateAction,
14230
14516
  generateOrgSettingsCmd: _generateOrgSettingsCmd,
14231
14517
  setOdtChatMode: _setOdtChatMode,
14232
14518
  v2SaveOrgConfig: _v2SaveOrgConfig,