toolroll 0.9.54 → 0.9.56

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -12
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/agent-onboard.js +4 -1
  6. package/dist/approval-policy.d.ts +5 -5
  7. package/dist/approval-policy.js +4 -4
  8. package/dist/approval-rules-ui.d.ts +3 -2
  9. package/dist/approval-rules-ui.js +17 -22
  10. package/dist/assignment-ui.d.ts +3 -2
  11. package/dist/assignment-ui.js +4 -11
  12. package/dist/backup-ui.d.ts +2 -1
  13. package/dist/backup-ui.js +16 -27
  14. package/dist/brand-mark.d.ts +3 -2
  15. package/dist/brand-mark.js +4 -4
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +25 -25
  18. package/dist/browser-shell.d.ts +2 -1
  19. package/dist/browser-shell.js +9 -9
  20. package/dist/browser-workspace.d.ts +14 -15
  21. package/dist/browser-workspace.js +0 -3
  22. package/dist/budget-alerts.js +3 -3
  23. package/dist/chat-actions.d.ts +23 -18
  24. package/dist/chat-actions.js +110 -88
  25. package/dist/chat-ask.d.ts +2 -2
  26. package/dist/chat-ask.js +5 -5
  27. package/dist/chat-channel.d.ts +15 -15
  28. package/dist/chat-channel.js +20 -20
  29. package/dist/chat-controls.d.ts +0 -4
  30. package/dist/chat-controls.js +0 -1
  31. package/dist/chat-core.js +1 -1
  32. package/dist/chat-delivery-state.js +2 -2
  33. package/dist/chat-delivery.d.ts +2 -2
  34. package/dist/chat-delivery.js +27 -34
  35. package/dist/chat-flow.js +3 -3
  36. package/dist/chat-messages.d.ts +1 -1
  37. package/dist/chat-messages.js +4 -4
  38. package/dist/chat-migration.d.ts +1 -1
  39. package/dist/chat-migration.js +11 -11
  40. package/dist/chat-polish.d.ts +5 -4
  41. package/dist/chat-polish.js +9 -10
  42. package/dist/chat-rooms.js +3 -3
  43. package/dist/chat-warmth.d.ts +2 -2
  44. package/dist/check-levels-ui.d.ts +2 -1
  45. package/dist/check-levels-ui.js +26 -53
  46. package/dist/cli.d.ts +3 -1
  47. package/dist/cli.js +15 -10
  48. package/dist/coding-shipping-ui.d.ts +2 -1
  49. package/dist/coding-shipping-ui.js +6 -6
  50. package/dist/coding-ui.d.ts +3 -1
  51. package/dist/coding-ui.js +30 -29
  52. package/dist/contracts/chat-actions.d.ts +32 -27
  53. package/dist/contracts/chat-actions.js +29 -11
  54. package/dist/contracts/chat-content.d.ts +1 -1
  55. package/dist/contracts/chat-content.js +1 -1
  56. package/dist/contracts/chat-tables.js +4 -4
  57. package/dist/contracts/cli.d.ts +1 -3
  58. package/dist/contracts/console-api.d.ts +7 -7
  59. package/dist/contracts/console-api.js +13 -12
  60. package/dist/contracts/flow-propose.d.ts +3 -3
  61. package/dist/contracts/flow.d.ts +132 -125
  62. package/dist/contracts/flow.js +27 -12
  63. package/dist/contracts/health.d.ts +1 -3
  64. package/dist/contracts/lead-context.d.ts +1 -1
  65. package/dist/contracts/lead-context.js +2 -2
  66. package/dist/contracts/lead-tools.d.ts +20 -18
  67. package/dist/contracts/lead-tools.js +15 -15
  68. package/dist/contracts/stage-output.d.ts +1 -1
  69. package/dist/contracts/stage-output.js +1 -1
  70. package/dist/contracts/store-json.d.ts +23 -22
  71. package/dist/contracts/store-json.js +17 -17
  72. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  73. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  74. package/dist/control-ui.d.ts +5 -4
  75. package/dist/control-ui.js +9 -34
  76. package/dist/converse.d.ts +23 -23
  77. package/dist/converse.js +28 -28
  78. package/dist/coordinator-proposals.d.ts +2 -2
  79. package/dist/coordinator-proposals.js +1 -1
  80. package/dist/coordinator.js +1 -1
  81. package/dist/credentials-ui.d.ts +5 -4
  82. package/dist/credentials-ui.js +23 -24
  83. package/dist/demo-chat.d.ts +3 -3
  84. package/dist/demo-chat.js +29 -76
  85. package/dist/deprecations.d.ts +12 -0
  86. package/dist/deprecations.js +20 -0
  87. package/dist/desktop-update-gate.js +8 -4
  88. package/dist/desktop-update.js +6 -1
  89. package/dist/discord-chat.js +6 -6
  90. package/dist/discord-settings.d.ts +3 -2
  91. package/dist/discord-settings.js +17 -42
  92. package/dist/dispatch.d.ts +1 -1
  93. package/dist/dispatch.js +1 -1
  94. package/dist/envelope.js +10 -1
  95. package/dist/evidence-pack.d.ts +2 -1
  96. package/dist/evidence-pack.js +45 -49
  97. package/dist/export-ui.d.ts +7 -2
  98. package/dist/export-ui.js +4 -10
  99. package/dist/export.js +7 -7
  100. package/dist/flow-engine.d.ts +2 -2
  101. package/dist/flow-engine.js +21 -21
  102. package/dist/flow-gallery-ui.d.ts +4 -3
  103. package/dist/flow-gallery-ui.js +28 -43
  104. package/dist/flow-insights.d.ts +1 -1
  105. package/dist/flow-share.js +13 -8
  106. package/dist/flow-starters-ui.d.ts +2 -1
  107. package/dist/flow-starters-ui.js +12 -15
  108. package/dist/flow-steps.d.ts +5 -5
  109. package/dist/flow-steps.js +19 -19
  110. package/dist/flow-triggers.d.ts +2 -1
  111. package/dist/flow-triggers.js +17 -15
  112. package/dist/flows-cli.js +1 -1
  113. package/dist/flows-ui.d.ts +4 -3
  114. package/dist/flows-ui.js +38 -49
  115. package/dist/flows.js +21 -20
  116. package/dist/guides.js +112 -6
  117. package/dist/html.d.ts +76 -0
  118. package/dist/html.js +173 -0
  119. package/dist/integrations-ui.d.ts +2 -1
  120. package/dist/integrations-ui.js +19 -23
  121. package/dist/invoke.js +1 -1
  122. package/dist/kits-ui.d.ts +5 -4
  123. package/dist/kits-ui.js +22 -24
  124. package/dist/kits.d.ts +12 -12
  125. package/dist/kits.js +31 -31
  126. package/dist/knowledge-ui.d.ts +6 -5
  127. package/dist/knowledge-ui.js +26 -25
  128. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  129. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  130. package/dist/lead-commitments.js +4 -4
  131. package/dist/lead-context.d.ts +12 -2
  132. package/dist/lead-context.js +15 -17
  133. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  134. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  135. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  136. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  137. package/dist/lead-follow.d.ts +5 -5
  138. package/dist/lead-follow.js +9 -9
  139. package/dist/lead-people.d.ts +5 -5
  140. package/dist/lead-people.js +14 -14
  141. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  142. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  143. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  144. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  145. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  146. package/dist/{mate.js → lead.js} +102 -102
  147. package/dist/ledger-view.d.ts +2 -1
  148. package/dist/ledger-view.js +27 -29
  149. package/dist/limits-ui.d.ts +3 -2
  150. package/dist/limits-ui.js +8 -8
  151. package/dist/live-bus.d.ts +50 -8
  152. package/dist/live-bus.js +133 -50
  153. package/dist/mcp-connect.d.ts +1 -1
  154. package/dist/mcp-connect.js +3 -3
  155. package/dist/mcp-core.js +1 -1
  156. package/dist/mcp-oauth.js +35 -36
  157. package/dist/memory-pass.d.ts +2 -2
  158. package/dist/memory-pass.js +7 -7
  159. package/dist/models-ui.d.ts +2 -1
  160. package/dist/models-ui.js +25 -35
  161. package/dist/monitoring-ui.d.ts +4 -3
  162. package/dist/monitoring-ui.js +13 -26
  163. package/dist/notification-rows.d.ts +2 -2
  164. package/dist/openrouter-models.d.ts +2 -1
  165. package/dist/openrouter-models.js +5 -9
  166. package/dist/operate.d.ts +3 -3
  167. package/dist/operate.js +46 -41
  168. package/dist/people-audit-ui.d.ts +2 -1
  169. package/dist/people-audit-ui.js +13 -13
  170. package/dist/policy-ui.d.ts +3 -2
  171. package/dist/policy-ui.js +17 -23
  172. package/dist/policy.d.ts +1 -1
  173. package/dist/principal.d.ts +1 -1
  174. package/dist/principal.js +2 -2
  175. package/dist/project-delete-ui.d.ts +4 -3
  176. package/dist/project-delete-ui.js +16 -28
  177. package/dist/project-delete.d.ts +1 -1
  178. package/dist/project-delete.js +26 -26
  179. package/dist/project-memory.js +1 -1
  180. package/dist/project-tools.d.ts +1 -1
  181. package/dist/project-tools.js +1 -1
  182. package/dist/pull-request-ui.d.ts +2 -1
  183. package/dist/pull-request-ui.js +17 -32
  184. package/dist/recipe-ui.d.ts +8 -7
  185. package/dist/recipe-ui.js +39 -32
  186. package/dist/release-adapters.d.ts +60 -0
  187. package/dist/release-adapters.js +435 -0
  188. package/dist/release-cli.d.ts +24 -0
  189. package/dist/release-cli.js +181 -0
  190. package/dist/release-gate.d.ts +80 -0
  191. package/dist/release-gate.js +150 -0
  192. package/dist/release-links.d.ts +29 -0
  193. package/dist/release-links.js +102 -0
  194. package/dist/release.d.ts +331 -0
  195. package/dist/release.js +550 -0
  196. package/dist/reply-shape.d.ts +2 -1
  197. package/dist/reply-shape.js +5 -5
  198. package/dist/repository-context-ui.d.ts +2 -1
  199. package/dist/repository-context-ui.js +2 -7
  200. package/dist/request-budget-ui.d.ts +2 -1
  201. package/dist/request-budget-ui.js +16 -16
  202. package/dist/result-review.d.ts +2 -3
  203. package/dist/result-review.js +3 -9
  204. package/dist/retention-ui.d.ts +7 -1
  205. package/dist/retention-ui.js +7 -11
  206. package/dist/retention.js +4 -4
  207. package/dist/scope.d.ts +2 -2
  208. package/dist/scout-report.d.ts +1 -1
  209. package/dist/scout-report.js +1 -1
  210. package/dist/serve.d.ts +11 -2
  211. package/dist/serve.js +105 -99
  212. package/dist/server/chat.d.ts +7 -10
  213. package/dist/server/chat.js +984 -1021
  214. package/dist/server/chrome.d.ts +291 -0
  215. package/dist/server/chrome.js +747 -0
  216. package/dist/server/console.css +2819 -0
  217. package/dist/server/flows.d.ts +2 -6
  218. package/dist/server/flows.js +854 -773
  219. package/dist/server/guards.d.ts +1 -1
  220. package/dist/server/guards.js +8 -5
  221. package/dist/server/handler-context.d.ts +1 -1
  222. package/dist/server/handler-context.js +1 -1
  223. package/dist/server/handler-registry.d.ts +8 -6
  224. package/dist/server/handler-registry.js +14 -2
  225. package/dist/server/http.d.ts +274 -0
  226. package/dist/server/http.js +239 -0
  227. package/dist/server/live.d.ts +34 -0
  228. package/dist/server/live.js +309 -0
  229. package/dist/server/pages.d.ts +2 -6
  230. package/dist/server/pages.js +808 -789
  231. package/dist/server/people-tokens.d.ts +1 -6
  232. package/dist/server/people-tokens.js +581 -573
  233. package/dist/server/remote.d.ts +4 -7
  234. package/dist/server/remote.js +39 -70
  235. package/dist/server/render-chat.d.ts +402 -0
  236. package/dist/server/render-chat.js +927 -0
  237. package/dist/server/render-pages.d.ts +535 -0
  238. package/dist/server/render-pages.js +1277 -0
  239. package/dist/server/render-people.d.ts +32 -0
  240. package/dist/server/render-people.js +121 -0
  241. package/dist/server/render-results.d.ts +744 -0
  242. package/dist/server/render-results.js +2237 -0
  243. package/dist/server/render-settings.d.ts +154 -0
  244. package/dist/server/render-settings.js +580 -0
  245. package/dist/server/render-tasks.d.ts +632 -0
  246. package/dist/server/render-tasks.js +2345 -0
  247. package/dist/server/request-context.d.ts +46 -0
  248. package/dist/server/request-context.js +17 -0
  249. package/dist/server/route-table.d.ts +25 -3
  250. package/dist/server/route-table.js +167 -77
  251. package/dist/server/runtime.d.ts +28 -20
  252. package/dist/server/runtime.js +7 -1
  253. package/dist/server/session.d.ts +138 -0
  254. package/dist/server/session.js +94 -0
  255. package/dist/server/settings.d.ts +1 -5
  256. package/dist/server/settings.js +2186 -2121
  257. package/dist/server/task-views.d.ts +11 -4
  258. package/dist/server/task-views.js +26 -22
  259. package/dist/server/tasks.d.ts +9 -9
  260. package/dist/server/tasks.js +1489 -1420
  261. package/dist/server-telemetry.d.ts +4 -3
  262. package/dist/server-telemetry.js +4 -8
  263. package/dist/setup-guide.d.ts +2 -2
  264. package/dist/skills-ui.d.ts +4 -3
  265. package/dist/skills-ui.js +14 -17
  266. package/dist/skills.d.ts +1 -1
  267. package/dist/skills.js +2 -1
  268. package/dist/slack-chat.d.ts +2 -2
  269. package/dist/slack-chat.js +6 -6
  270. package/dist/slack-settings.d.ts +3 -2
  271. package/dist/slack-settings.js +17 -42
  272. package/dist/spend-ui.d.ts +5 -4
  273. package/dist/spend-ui.js +17 -40
  274. package/dist/spend.d.ts +14 -14
  275. package/dist/spend.js +31 -31
  276. package/dist/sso-ui.d.ts +3 -2
  277. package/dist/sso-ui.js +26 -27
  278. package/dist/storage-ui.d.ts +2 -1
  279. package/dist/storage-ui.js +14 -33
  280. package/dist/store-baseline.d.ts +19 -0
  281. package/dist/store-baseline.js +2873 -0
  282. package/dist/store.d.ts +197 -249
  283. package/dist/store.js +747 -3084
  284. package/dist/subagent-admin.d.ts +53 -0
  285. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  286. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  287. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  288. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  289. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  290. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  291. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  292. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  293. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  294. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  295. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  296. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  297. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  298. package/dist/subagents-ui.d.ts +22 -0
  299. package/dist/subagents-ui.js +137 -0
  300. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  301. package/dist/{teammates.js → subagents.js} +21 -21
  302. package/dist/subscription-chat.d.ts +13 -13
  303. package/dist/subscription-chat.js +19 -19
  304. package/dist/surface.d.ts +19 -2
  305. package/dist/surface.js +20 -33
  306. package/dist/task-composer.d.ts +2 -1
  307. package/dist/task-composer.js +7 -7
  308. package/dist/task-status.d.ts +4 -3
  309. package/dist/task-status.js +15 -16
  310. package/dist/team-cli.d.ts +1 -0
  311. package/dist/team-cli.js +14 -2
  312. package/dist/team-http.d.ts +5 -9
  313. package/dist/team-http.js +5 -63
  314. package/dist/team-leads.js +13 -13
  315. package/dist/team-runtime.d.ts +2 -2
  316. package/dist/team-runtime.js +8 -8
  317. package/dist/team-ui.d.ts +2 -1
  318. package/dist/team-ui.js +2 -5
  319. package/dist/teams-chat.js +6 -6
  320. package/dist/teams-settings.d.ts +3 -2
  321. package/dist/teams-settings.js +20 -31
  322. package/dist/telegram-flow.d.ts +3 -3
  323. package/dist/telegram-flow.js +3 -3
  324. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  325. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  326. package/dist/telegram-settings.d.ts +3 -2
  327. package/dist/telegram-settings.js +14 -25
  328. package/dist/telegram-status.js +1 -1
  329. package/dist/telegram-team.d.ts +1 -1
  330. package/dist/telegram-team.js +5 -5
  331. package/dist/telegram.d.ts +1 -1
  332. package/dist/telegram.js +9 -30
  333. package/dist/text-limits.d.ts +18 -18
  334. package/dist/text-limits.js +18 -18
  335. package/dist/toolroll-update-ui.d.ts +3 -2
  336. package/dist/toolroll-update-ui.js +35 -39
  337. package/dist/toolroll-update.d.ts +8 -0
  338. package/dist/toolroll-update.js +45 -8
  339. package/dist/tools-ui.d.ts +5 -2
  340. package/dist/tools-ui.js +31 -31
  341. package/dist/when-html.d.ts +11 -3
  342. package/dist/when-html.js +5 -6
  343. package/dist/workspace-ui.d.ts +4 -2
  344. package/dist/workspace-ui.js +10 -10
  345. package/package.json +1 -2
  346. package/dist/server/shared.d.ts +0 -3151
  347. package/dist/server/shared.js +0 -12001
  348. package/dist/teammate-admin.d.ts +0 -53
  349. package/dist/teammates-ui.d.ts +0 -20
  350. package/dist/teammates-ui.js +0 -166
@@ -1,4 +1,5 @@
1
1
  import type { LedgerEntry } from "./action-ledger.js";
2
+ import { type Html } from "./html.js";
2
3
  /** v103: the chain's state in one line, and (for an instance operator) a checkpoint to copy off the machine. */
3
4
  export type LedgerChainView = {
4
5
  ok: boolean;
@@ -21,4 +22,4 @@ export type LedgerChainView = {
21
22
  /** Today (UTC, YYYY-MM-DD): the audit export's default range ends here. */
22
23
  today: string;
23
24
  };
24
- export declare function ledgerBody(rows: LedgerEntry[], projects: readonly string[], params: URLSearchParams, chain?: LedgerChainView): string;
25
+ export declare function ledgerBody(rows: LedgerEntry[], projects: readonly string[], params: URLSearchParams, chain?: LedgerChainView): Html;
@@ -1,23 +1,28 @@
1
1
  import { projectName } from "./project.js";
2
2
  import { exactWhenHtml } from "./when-html.js";
3
- const html = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3
+ import { html, postForm, styleElement } from "./html.js";
4
4
  /** The ledger's kinds of event, as the filter names them. */
5
5
  const EVENT_KINDS = [["work", "Work"], ["request", "Requests"], ["access", "Access"], ["sign-in", "Sign-ins"], ["policy", "Policy changes"]];
6
6
  function chainHtml(chain) {
7
- const html = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8
7
  const status = chain.ok
9
- ? `<p class="ledger-chain ok" data-ledger-chain="ok"><strong>Chain verified</strong> · ${chain.entries} entries through #${chain.through ?? 0} · head <code>${html(chain.head.slice(0, 16))}…</code>${chain.checkedAt === null ? "" : ` · checked in full ${html(chain.checkedAt.slice(11, 16))} UTC`}</p>`
10
- : `<p class="ledger-chain problem" role="alert" data-ledger-chain="broken"><strong>Chain broken</strong> · ${html(chain.problem?.what ?? "the chain doesn't verify")}</p>`;
11
- const latest = chain.latest === null ? "" : `<p class="meta ledger-checkpoint">Latest checkpoint (${html(chain.latest.at.slice(0, 16).replace("T", " "))} UTC), copy it somewhere outside this machine: <code class="ledger-checkpoint-value">${chain.latest.through}:${html(chain.latest.hash)}</code></p>`;
12
- const make = chain.csrf === null ? "" : `<form method="post" action="/ledger/checkpoint" class="ledger-checkpoint-form"><input type="hidden" name="csrf" value="${html(chain.csrf)}"><button type="submit">Make a checkpoint</button></form>`;
8
+ ? html `<p class="ledger-chain ok" data-ledger-chain="ok"><strong>Chain verified</strong> · ${chain.entries} entries through #${chain.through ?? 0} · head <code>${chain.head.slice(0, 16)}…</code>${chain.checkedAt === null ? "" : ` · checked in full ${chain.checkedAt.slice(11, 16)} UTC`}</p>`
9
+ : html `<p class="ledger-chain problem" role="alert" data-ledger-chain="broken"><strong>Chain broken</strong> · ${chain.problem?.what ?? "the chain doesn't verify"}</p>`;
10
+ const latest = chain.latest === null ? "" : html `<p class="meta ledger-checkpoint">Latest checkpoint (${chain.latest.at.slice(0, 16).replace("T", " ")} UTC), copy it somewhere outside this machine: <code class="ledger-checkpoint-value">${chain.latest.through}:${chain.latest.hash}</code></p>`;
11
+ const make = chain.csrf === null ? "" : postForm("/ledger/checkpoint", html `<button type="submit">Make a checkpoint</button>`, { attrs: { class: "ledger-checkpoint-form" } });
13
12
  const start = new Date(Date.parse(`${chain.today}T00:00:00Z`) - 29 * 86_400_000).toISOString().slice(0, 10);
14
- const exportForm = `<details class="ledger-filter-panel"><summary>Audit export</summary><form method="get" action="/ledger/export" class="ledger-filters ledger-export" aria-label="Audit export">` +
15
- `<label>From<input type="date" name="from" value="${html(start)}" required></label><label>To<input type="date" name="to" value="${html(chain.today)}" required></label>` +
16
- `<button type="submit">Download</button></form><p class="meta">Every entry in the range with its seal, and an evidence pack for each task it names. JSON.</p></details>`;
17
- return `<section class="ledger-chain-panel" aria-label="Ledger integrity">${status}${latest}${make}${exportForm}</section>`;
13
+ const exportForm = html `<details class="ledger-filter-panel"><summary>Audit export</summary><form method="get" action="/ledger/export" class="ledger-filters ledger-export" aria-label="Audit export"><label>From<input type="date" name="from" value="${start}" required></label><label>To<input type="date" name="to" value="${chain.today}" required></label><button type="submit">Download</button></form><p class="meta">Every entry in the range with its seal, and an evidence pack for each task it names. JSON.</p></details>`;
14
+ return html `<section class="ledger-chain-panel" aria-label="Ledger integrity">${status}${latest}${make}${exportForm}</section>`;
18
15
  }
16
+ const LEDGER_CSS = `
17
+ .ledger-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ledger-heading h1{margin:0}.ledger-caption{margin:8px 0 12px}
18
+ .ledger-filter-panel{margin-bottom:12px}.ledger-filter-panel summary{font-size:13px;padding:6px 0;cursor:pointer}.ledger-filters{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) auto auto;gap:8px;align-items:end;margin:0 0 12px}.ledger-filters label{display:grid;gap:3px;min-width:0;font-size:12px}.ledger-filters input,.ledger-filters select{width:100%;min-width:0;box-sizing:border-box;margin:0;padding:6px 8px;font-size:13px}.ledger-filters button{padding:7px 12px}.ledger-filters>a{padding:8px 0;font-size:13px}
19
+ .ledger-detail{display:block;color:var(--muted-foreground);font-size:12px}.ledger-list{border:1px solid var(--border);border-radius:8px;overflow:hidden}.ledger-event{display:grid;grid-template-columns:140px minmax(90px,1fr) minmax(160px,2fr) minmax(100px,1fr) minmax(100px,1fr);gap:8px;align-items:baseline;padding:7px 10px;border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;overflow-wrap:anywhere}.ledger-event:last-child{border-bottom:0}.ledger-event .actor{color:inherit;font-weight:600;text-decoration:none}.ledger-time,.ledger-place,.ledger-result small{font-size:11px;color:var(--muted-foreground)}.ledger-time time{font-variant-numeric:tabular-nums}.ledger-place a{display:block}.ledger-result small{display:block}.ledger-labels{font-size:11px;color:var(--muted-foreground);font-weight:600}.ledger-empty{padding:12px}
20
+ @media(max-width:1000px){.ledger-filters{grid-template-columns:repeat(3,minmax(0,1fr)) auto}.ledger-event{grid-template-columns:122px minmax(80px,1fr) minmax(130px,2fr) minmax(85px,1fr)}.ledger-place{grid-column:2/4}.ledger-result{grid-column:4;grid-row:1/3}.ledger-labels{display:none}}
21
+ @media(max-width:600px){.ledger-filters{grid-template-columns:repeat(2,minmax(0,1fr))}.ledger-event{grid-template-columns:minmax(0,1fr) auto;gap:2px 8px;padding:8px 10px}.ledger-actor{grid-column:1;grid-row:1}.ledger-action{grid-column:1;grid-row:2}.ledger-result{grid-column:2;grid-row:1/3;text-align:right}.ledger-time{grid-column:1;grid-row:3}.ledger-place{grid-column:2;grid-row:3;text-align:right}.ledger-place a{display:inline}.ledger-heading>a{font-size:13px}}
22
+ .ledger-chain-panel{margin:0 0 12px}.ledger-chain{margin:0 0 4px;font-size:13px}.ledger-chain.ok strong{color:var(--success)}.ledger-checkpoint code{overflow-wrap:anywhere;user-select:all}.ledger-checkpoint-form{margin:6px 0 0}.ledger-export{grid-template-columns:repeat(2,minmax(0,180px)) auto;margin-bottom:4px}
23
+ `;
19
24
  export function ledgerBody(rows, projects, params, chain) {
20
- const selected = (key, value) => params.get(key) === value ? " selected" : "";
25
+ const selected = (key, value) => params.get(key) === value ? html ` selected` : "";
21
26
  const more = rows.length > 50;
22
27
  const shown = rows.slice(0, 50);
23
28
  const fresh = new URLSearchParams();
@@ -31,29 +36,22 @@ export function ledgerBody(rows, projects, params, chain) {
31
36
  next.set("before", String(shown.at(-1).id));
32
37
  const csv = new URLSearchParams(fresh);
33
38
  csv.set("format", "csv");
34
- return `<style>
35
- .ledger-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ledger-heading h1{margin:0}.ledger-caption{margin:8px 0 12px}
36
- .ledger-filter-panel{margin-bottom:12px}.ledger-filter-panel summary{font-size:13px;padding:6px 0;cursor:pointer}.ledger-filters{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) auto auto;gap:8px;align-items:end;margin:0 0 12px}.ledger-filters label{display:grid;gap:3px;min-width:0;font-size:12px}.ledger-filters input,.ledger-filters select{width:100%;min-width:0;box-sizing:border-box;margin:0;padding:6px 8px;font-size:13px}.ledger-filters button{padding:7px 12px}.ledger-filters>a{padding:8px 0;font-size:13px}
37
- .ledger-detail{display:block;color:var(--muted-foreground);font-size:12px}.ledger-list{border:1px solid var(--border);border-radius:8px;overflow:hidden}.ledger-event{display:grid;grid-template-columns:140px minmax(90px,1fr) minmax(160px,2fr) minmax(100px,1fr) minmax(100px,1fr);gap:8px;align-items:baseline;padding:7px 10px;border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;overflow-wrap:anywhere}.ledger-event:last-child{border-bottom:0}.ledger-event .actor{color:inherit;font-weight:600;text-decoration:none}.ledger-time,.ledger-place,.ledger-result small{font-size:11px;color:var(--muted-foreground)}.ledger-time time{font-variant-numeric:tabular-nums}.ledger-place a{display:block}.ledger-result small{display:block}.ledger-labels{font-size:11px;color:var(--muted-foreground);font-weight:600}.ledger-empty{padding:12px}
38
- @media(max-width:1000px){.ledger-filters{grid-template-columns:repeat(3,minmax(0,1fr)) auto}.ledger-event{grid-template-columns:122px minmax(80px,1fr) minmax(130px,2fr) minmax(85px,1fr)}.ledger-place{grid-column:2/4}.ledger-result{grid-column:4;grid-row:1/3}.ledger-labels{display:none}}
39
- @media(max-width:600px){.ledger-filters{grid-template-columns:repeat(2,minmax(0,1fr))}.ledger-event{grid-template-columns:minmax(0,1fr) auto;gap:2px 8px;padding:8px 10px}.ledger-actor{grid-column:1;grid-row:1}.ledger-action{grid-column:1;grid-row:2}.ledger-result{grid-column:2;grid-row:1/3;text-align:right}.ledger-time{grid-column:1;grid-row:3}.ledger-place{grid-column:2;grid-row:3;text-align:right}.ledger-place a{display:inline}.ledger-heading>a{font-size:13px}}
40
- .ledger-chain-panel{margin:0 0 12px}.ledger-chain{margin:0 0 4px;font-size:13px}.ledger-chain.ok strong{color:var(--success)}.ledger-checkpoint code{overflow-wrap:anywhere;user-select:all}.ledger-checkpoint-form{margin:6px 0 0}.ledger-export{grid-template-columns:repeat(2,minmax(0,180px)) auto;margin-bottom:4px}
41
- </style><div class="ledger-heading"><h1>Action ledger</h1><a href="/ledger?${html(csv.toString())}" download>Export CSV</a></div>${chain === undefined ? "" : chainHtml(chain)}
39
+ return html `${styleElement(LEDGER_CSS)}<div class="ledger-heading"><h1>Action ledger</h1><a href="/ledger?${csv.toString()}" download>Export CSV</a></div>${chain === undefined ? "" : chainHtml(chain)}
42
40
  <p class="meta ledger-caption">${shown.length} actions on this page · CSV includes all matching actions · Times in UTC</p>
43
41
  <details class="ledger-filter-panel"><summary>Filters · ${fresh.size === 0 ? "all actions" : `${fresh.size} active`}</summary>
44
42
  <form method="get" action="/ledger" class="ledger-filters" aria-label="Filter action ledger">
45
- <label>Project<select name="project"><option value="">All accessible projects</option>${projects.map(repo => `<option value="${html(repo)}"${selected("project", repo)}>${html(projectName(repo))}</option>`).join("")}</select></label>
46
- <label>Person or worker<input type="text" name="actor" value="${html(params.get("actor") ?? "")}" placeholder="Any actor"></label>
47
- <label>Events<select name="source"><option value="">All events</option>${EVENT_KINDS.map(([value, label]) => `<option value="${value}"${selected("source", value)}>${label}</option>`).join("")}</select></label>
48
- <label>Outcome<input type="text" name="outcome" value="${html(params.get("outcome") ?? "")}" placeholder="Any outcome"></label>
49
- <label>Task<input type="text" name="task" value="${html(params.get("task") ?? "")}" placeholder="Any task"></label>
43
+ <label>Project<select name="project"><option value="">All accessible projects</option>${projects.map(repo => html `<option value="${repo}"${selected("project", repo)}>${projectName(repo)}</option>`)}</select></label>
44
+ <label>Person or worker<input type="text" name="actor" value="${params.get("actor") ?? ""}" placeholder="Any actor"></label>
45
+ <label>Events<select name="source"><option value="">All events</option>${EVENT_KINDS.map(([value, label]) => html `<option value="${value}"${selected("source", value)}>${label}</option>`)}</select></label>
46
+ <label>Outcome<input type="text" name="outcome" value="${params.get("outcome") ?? ""}" placeholder="Any outcome"></label>
47
+ <label>Task<input type="text" name="task" value="${params.get("task") ?? ""}" placeholder="Any task"></label>
50
48
  <button type="submit">Filter</button><a href="/ledger">Clear</a></form></details>
51
- <div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time</span><span>Actor</span><span>Action</span><span>Project / work</span><span>Outcome / event</span></div>${shown.length === 0 ? `<p class="ledger-empty">No actions match these filters yet.</p>` : shown.map(row => {
52
- const subject = row.runId !== null ? `<a href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? `<a href="/t/${encodeURIComponent(row.taskId)}">${html(row.taskId)}</a>` : "";
49
+ <div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time</span><span>Actor</span><span>Action</span><span>Project / work</span><span>Outcome / event</span></div>${shown.length === 0 ? html `<p class="ledger-empty">No actions match these filters yet.</p>` : shown.map(row => {
50
+ const subject = row.runId !== null ? html `<a href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? html `<a href="/t/${encodeURIComponent(row.taskId)}">${row.taskId}</a>` : "";
53
51
  const byActor = new URLSearchParams(fresh);
54
52
  byActor.set("actor", row.actor);
55
- return `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time">${exactWhenHtml(row.at)}</div><div class="ledger-actor"><a class="actor" href="/ledger?${html(byActor.toString())}" title="Filter by this actor">${html(row.actor)}</a></div><div class="ledger-action">${html(row.action)}${row.detail === null ? "" : `<small class="ledger-detail">${html(row.detail)}</small>`}</div><div class="ledger-place">${row.repo === null ? "Instance" : html(projectName(row.repo))} ${subject}</div><div class="ledger-result">${html(row.outcome)}<small>${html(row.source)}</small></div></article>`;
56
- }).join("")}</div>
57
- <p class="row"><a href="/ledger?${html(fresh.toString())}">Newest actions</a>${more ? `<a rel="next" href="/ledger?${html(next.toString())}">Older actions</a>` : ""}</p>
53
+ return html `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time">${exactWhenHtml(row.at)}</div><div class="ledger-actor"><a class="actor" href="/ledger?${byActor.toString()}" title="Filter by this actor">${row.actor}</a></div><div class="ledger-action">${row.action}${row.detail === null ? "" : html `<small class="ledger-detail">${row.detail}</small>`}</div><div class="ledger-place">${row.repo === null ? "Instance" : projectName(row.repo)} ${subject}</div><div class="ledger-result">${row.outcome}<small>${row.source}</small></div></article>`;
54
+ })}</div>
55
+ <p class="row"><a href="/ledger?${fresh.toString()}">Newest actions</a>${more ? html `<a rel="next" href="/ledger?${next.toString()}">Older actions</a>` : ""}</p>
58
56
  <p class="meta">Accepted requests record acceptance; worker events record results. History starts from this upgrade. Review evidence is on each task or run.</p>`;
59
57
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { BrowserLimits } from "./browser-workspace.js";
8
8
  import { type BudgetState } from "./spend.js";
9
+ import { type Html } from "./html.js";
9
10
  /** "in 25 min", "in 2 h 10 min", or a day and hour ("Fri 11 PM") beyond a day. */
10
11
  export declare function resetWords(resetsAt: string, now: Date): string;
11
12
  export declare function limitsView(windows: readonly {
@@ -19,8 +20,8 @@ export declare function limitsView(windows: readonly {
19
20
  observedAt: string;
20
21
  }[], budgets: readonly BudgetState[], names: {
21
22
  project: (repo: string) => string;
22
- teammate: (id: number) => string;
23
+ subagent: (id: number) => string;
23
24
  }, now: Date): BrowserLimits | null;
24
25
  /** The same tiles for the page without scripts. */
25
- export declare function limitsHtml(limits: BrowserLimits | null): string;
26
+ export declare function limitsHtml(limits: BrowserLimits | null): Html;
26
27
  export declare const LIMITS_CSS: string;
package/dist/limits-ui.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { windowLabel } from "./provider-limits.js";
2
2
  import { usd } from "./spend.js";
3
+ import { html } from "./html.js";
3
4
  const PROVIDER_NAMES = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
4
5
  /** Plan names worth showing; anything else (an internal billing name) is left off. */
5
6
  const PLAN_NAMES = { free: "Free", plus: "Plus", pro: "Pro", max: "Max", team: "Team", business: "Business", enterprise: "Enterprise", edu: "Edu" };
@@ -37,7 +38,7 @@ export function limitsView(windows, budgets, names, now) {
37
38
  for (const budget of budgets) {
38
39
  const over = budget.spentMicrousd >= budget.limitMicrousd;
39
40
  const name = budget.scope === "installation" ? "Everything" : budget.scope === "project" ? names.project(budget.key)
40
- : budget.scope === "teammate" ? names.teammate(Number(budget.key)) : budget.key;
41
+ : budget.scope === "subagent" ? names.subagent(Number(budget.key)) : budget.key;
41
42
  tiles.push({
42
43
  key: `budget:${budget.id}`, name, window: "Budget", value: usd(budget.spentMicrousd), unit: `of ${usd(budget.limitMicrousd)}`,
43
44
  percent: budget.percent, tone: over ? (budget.hardStop ? "danger" : "warning") : budget.percent >= 80 ? "warning" : "neutral", marks: [50, 80], href: "/spend",
@@ -47,16 +48,15 @@ export function limitsView(windows, budgets, names, now) {
47
48
  }
48
49
  return tiles.length === 0 ? null : { tiles };
49
50
  }
50
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
51
51
  /** The same tiles for the page without scripts. */
52
52
  export function limitsHtml(limits) {
53
53
  if (limits === null)
54
- return "";
55
- return `<section class="limits" aria-label="Limits"><ul>${limits.tiles.map(tile => {
56
- const body = `<p class="limit-name"><strong>${e(tile.name)}</strong> ${e(tile.window)}</p><p class="limit-value"><strong>${e(tile.value)}</strong> ${e(tile.unit)}</p>` +
57
- `<span class="limit-bar" aria-hidden="true"><span style="width:${Math.min(100, Math.max(0, tile.percent))}%"></span></span><p class="limit-detail">${e(tile.detail)}</p>`;
58
- return `<li class="limit limit-${tile.tone}" data-limit="${e(tile.key)}"${tile.title === null ? "" : ` title="${e(tile.title)}"`}>${tile.href === null ? body : `<a href="${e(tile.href)}">${body}</a>`}</li>`;
59
- }).join("")}</ul></section>`;
54
+ return html ``;
55
+ return html `<section class="limits" aria-label="Limits"><ul>${limits.tiles.map(tile => {
56
+ const body = html `<p class="limit-name"><strong>${tile.name}</strong> ${tile.window}</p><p class="limit-value"><strong>${tile.value}</strong> ${tile.unit}</p>\
57
+ <span class="limit-bar" aria-hidden="true"><span style="width:${Math.min(100, Math.max(0, tile.percent))}%"></span></span><p class="limit-detail">${tile.detail}</p>`;
58
+ return html `<li class="limit limit-${tile.tone}" data-limit="${tile.key}"${tile.title === null ? "" : html ` title="${tile.title}"`}>${tile.href === null ? body : html `<a href="${tile.href}">${body}</a>`}</li>`;
59
+ })}</ul></section>`;
60
60
  }
61
61
  export const LIMITS_CSS = `.limits ul{list-style:none;margin:0 0 16px;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(176px,1fr));gap:8px}` +
62
62
  `.limit{border:1px solid var(--border);border-radius:10px;background:var(--card);padding:12px 14px;min-width:0}.limit a{color:inherit;text-decoration:none;display:block}` +
@@ -15,7 +15,10 @@
15
15
  * an invalidation, never content. The page reads itself again under its
16
16
  * own checks, so redaction stays the page's.
17
17
  * - The safety net. Every room also checks itself about every 30 s, so a
18
- * missed signal heals, and sends a keep-alive between.
18
+ * missed signal heals; the page's connection sends a keep-alive between,
19
+ * each after every room proves its viewer again.
20
+ * - One connection per page. A page opens one stream (GET /live) and joins
21
+ * every room it shows; each event names its room.
19
22
  * - Back-pressure. Each page's stream is bounded: a page that falls behind
20
23
  * gets one `reload` and ends so its EventSource can reconnect.
21
24
  *
@@ -67,23 +70,62 @@ export declare class LiveStream {
67
70
  send(event: string, data: unknown): void;
68
71
  keepAlive(): void;
69
72
  }
73
+ /**
74
+ * One page's live connection: its bounded stream, one keep-alive, and every
75
+ * room it joined. Each event names its room, so one EventSource serves the
76
+ * whole page. It ends when the page goes, when it falls behind (one
77
+ * `reload`), or when its last room lets it go. A keep-alive is a delivery
78
+ * too: every room proves its viewer again first, so a page whose session
79
+ * lapsed with nothing written hears its rooms go, never another frame.
80
+ */
81
+ export declare class LiveConnection {
82
+ readonly response: ServerResponse;
83
+ readonly stream: LiveStream;
84
+ private readonly leaves;
85
+ private readonly proofs;
86
+ private readonly timer;
87
+ private closed;
88
+ constructor(response: ServerResponse, options?: {
89
+ limitBytes?: number;
90
+ keepAliveMs?: number;
91
+ });
92
+ private keepAlive;
93
+ /** Run before each keep-alive: a room proves its viewer again (and leaves if it no longer passes). Returns the undo. */
94
+ onKeepAlive(prove: () => void): () => void;
95
+ get open(): boolean;
96
+ /** An event for one room: its data says which. */
97
+ send(room: string, event: string, data?: Record<string, unknown>): void;
98
+ /** Run when the connection ends; returns the undo for a room that leaves first. */
99
+ onClose(leave: () => void): () => void;
100
+ close(): void;
101
+ /** Rooms joined and not yet left. */
102
+ get rooms(): number;
103
+ }
104
+ /** One page in one room: who, the room's name as the page asked for it, the page's connection, and its re-proof. */
70
105
  export type LiveViewer = {
71
106
  name: string;
72
- response: ServerResponse;
107
+ room: string;
108
+ connection: LiveConnection;
73
109
  valid: () => boolean;
74
110
  };
75
111
  export type LiveRooms<K, V extends LiveViewer> = {
76
- /** Keep this page's stream on the room until it closes; the first events say where it stands and who's here. */
112
+ /** Keep this page on the room until it leaves; the first events say where the room stands (and who's here). */
77
113
  join: (key: K, viewer: V) => void;
78
- /** Every stream, ended (the server is closing). */
114
+ /** Every page leaves (the server is closing). */
79
115
  close: () => void;
80
116
  size: () => number;
81
117
  };
118
+ /** `minIntervalMs`: a busy room says `change` at most this often — the first at once, the latest after. */
82
119
  export type LiveRoomOptions = {
83
120
  bus?: LiveBus | null;
84
121
  safetyNetMs?: number;
85
- keepAliveMs?: number;
86
- limitBytes?: number;
122
+ minIntervalMs?: number;
87
123
  };
88
- /** Rooms of open pages that hear the bus. `presence` says who else is here, as each page should see it. */
89
- export declare function createLiveRooms<K, V extends LiveViewer>(fingerprint: (key: K) => string | null, presence: (viewers: readonly V[], me: string) => unknown, options?: LiveRoomOptions): LiveRooms<K, V>;
124
+ /**
125
+ * Rooms of open pages that hear the bus. `presence` says who else is here, as
126
+ * each page should see it (null: the room has no presence). A viewer that no
127
+ * longer passes its re-proof hears `gone` and leaves; a room whose subject is
128
+ * gone (fingerprint null) sends everyone `gone`. A connection with no rooms
129
+ * left ends.
130
+ */
131
+ export declare function createLiveRooms<K, V extends LiveViewer>(fingerprint: (key: K) => string | null, presence: ((viewers: readonly V[], me: string) => unknown) | null, options?: LiveRoomOptions): LiveRooms<K, V>;
package/dist/live-bus.js CHANGED
@@ -15,7 +15,10 @@
15
15
  * an invalidation, never content. The page reads itself again under its
16
16
  * own checks, so redaction stays the page's.
17
17
  * - The safety net. Every room also checks itself about every 30 s, so a
18
- * missed signal heals, and sends a keep-alive between.
18
+ * missed signal heals; the page's connection sends a keep-alive between,
19
+ * each after every room proves its viewer again.
20
+ * - One connection per page. A page opens one stream (GET /live) and joins
21
+ * every room it shows; each event names its room.
19
22
  * - Back-pressure. Each page's stream is bounded: a page that falls behind
20
23
  * gets one `reload` and ends so its EventSource can reconnect.
21
24
  *
@@ -183,34 +186,98 @@ export class LiveStream {
183
186
  this.write(": keep-alive\n\n");
184
187
  }
185
188
  }
189
+ /**
190
+ * One page's live connection: its bounded stream, one keep-alive, and every
191
+ * room it joined. Each event names its room, so one EventSource serves the
192
+ * whole page. It ends when the page goes, when it falls behind (one
193
+ * `reload`), or when its last room lets it go. A keep-alive is a delivery
194
+ * too: every room proves its viewer again first, so a page whose session
195
+ * lapsed with nothing written hears its rooms go, never another frame.
196
+ */
197
+ export class LiveConnection {
198
+ response;
199
+ stream;
200
+ leaves = new Set();
201
+ proofs = new Set();
202
+ timer;
203
+ closed = false;
204
+ constructor(response, options = {}) {
205
+ this.response = response;
206
+ this.stream = new LiveStream(response, options.limitBytes, () => this.close());
207
+ this.timer = setInterval(() => this.keepAlive(), options.keepAliveMs ?? KEEP_ALIVE_MS);
208
+ this.timer.unref();
209
+ response.once("close", () => this.close());
210
+ }
211
+ keepAlive() {
212
+ for (const prove of [...this.proofs]) {
213
+ if (!this.open)
214
+ return;
215
+ prove();
216
+ }
217
+ if (this.open)
218
+ this.stream.keepAlive();
219
+ }
220
+ /** Run before each keep-alive: a room proves its viewer again (and leaves if it no longer passes). Returns the undo. */
221
+ onKeepAlive(prove) { this.proofs.add(prove); return () => { this.proofs.delete(prove); }; }
222
+ get open() { return !this.closed && this.stream.open; }
223
+ /** An event for one room: its data says which. */
224
+ send(room, event, data = {}) { this.stream.send(event, { room, ...data }); }
225
+ /** Run when the connection ends; returns the undo for a room that leaves first. */
226
+ onClose(leave) { this.leaves.add(leave); return () => { this.leaves.delete(leave); }; }
227
+ close() {
228
+ if (this.closed)
229
+ return;
230
+ this.closed = true;
231
+ clearInterval(this.timer);
232
+ for (const leave of [...this.leaves])
233
+ leave();
234
+ this.leaves.clear();
235
+ this.proofs.clear();
236
+ if (!this.response.writableEnded)
237
+ this.response.end();
238
+ }
239
+ /** Rooms joined and not yet left. */
240
+ get rooms() { return this.leaves.size; }
241
+ }
186
242
  const PRESENCE_SETTLE_MS = 150;
187
- /** Rooms of open pages that hear the bus. `presence` says who else is here, as each page should see it. */
243
+ /**
244
+ * Rooms of open pages that hear the bus. `presence` says who else is here, as
245
+ * each page should see it (null: the room has no presence). A viewer that no
246
+ * longer passes its re-proof hears `gone` and leaves; a room whose subject is
247
+ * gone (fingerprint null) sends everyone `gone`. A connection with no rooms
248
+ * left ends.
249
+ */
188
250
  export function createLiveRooms(fingerprint, presence, options = {}) {
189
251
  const bus = options.bus ?? null;
190
- const keepAliveMs = options.keepAliveMs ?? KEEP_ALIVE_MS;
191
- const checkEvery = Math.max(1, Math.round((options.safetyNetMs ?? SAFETY_NET_MS) / keepAliveMs));
252
+ const safetyNetMs = options.safetyNetMs ?? SAFETY_NET_MS;
253
+ const minIntervalMs = options.minIntervalMs ?? 0;
192
254
  const rooms = new Map();
193
255
  let unsubscribe = null;
194
- const here = (room, viewer) => presence([...room.viewers.keys()], viewer.name);
195
- const announce = (room) => {
256
+ const announce = (key, room) => {
257
+ if (presence === null || rooms.get(key) !== room)
258
+ return;
196
259
  // A page that moves from one card to the next leaves and joins at once: say it once, after it settles.
197
260
  if (room.settling !== null)
198
261
  clearTimeout(room.settling);
199
262
  room.settling = setTimeout(() => {
200
263
  room.settling = null;
201
- for (const [viewer, stream] of room.viewers)
202
- stream.send("here", { people: here(room, viewer) });
264
+ for (const viewer of [...room.viewers.keys()])
265
+ deliver(key, room, viewer, "here", { people: presence([...room.viewers.keys()], viewer.name) });
203
266
  }, PRESENCE_SETTLE_MS);
204
267
  room.settling.unref();
205
268
  };
206
269
  const leave = (key, viewer) => {
207
270
  const room = rooms.get(key);
208
- if (room === undefined || !room.viewers.delete(viewer))
271
+ const forget = room?.viewers.get(viewer);
272
+ if (room === undefined || forget === undefined)
209
273
  return;
210
- if (!viewer.response.writableEnded)
211
- viewer.response.end();
274
+ room.viewers.delete(viewer);
275
+ forget();
276
+ // The page's last room: its connection has nothing left to say.
277
+ if (viewer.connection.rooms === 0)
278
+ viewer.connection.close();
212
279
  if (room.viewers.size > 0) {
213
- announce(room);
280
+ announce(key, room);
214
281
  return;
215
282
  }
216
283
  clearInterval(room.timer);
@@ -218,30 +285,46 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
218
285
  clearTimeout(room.settling);
219
286
  if (room.retry !== null)
220
287
  clearTimeout(room.retry);
288
+ if (room.holding !== null)
289
+ clearTimeout(room.holding);
221
290
  rooms.delete(key);
222
291
  if (rooms.size === 0 && unsubscribe !== null) {
223
292
  unsubscribe();
224
293
  unsubscribe = null;
225
294
  }
226
295
  };
227
- const drop = (key, viewer) => {
228
- rooms.get(key)?.viewers.get(viewer)?.send("gone", {});
229
- leave(key, viewer);
230
- };
231
- /** Recheck who may still see the room, then whether what it shows moved. */
232
- const refresh = (key, room, revision) => {
233
- // A signed-out or narrowed account stops hearing about it, at the first write after.
234
- for (const viewer of [...room.viewers.keys()]) {
235
- let ok = false;
296
+ /** The final delivery boundary, including queued frames and sends that may overflow. */
297
+ const deliver = (key, room, viewer, event, data = {}) => {
298
+ if (rooms.get(key) !== room || !room.viewers.has(viewer))
299
+ return false;
300
+ if (!viewer.connection.open) {
301
+ leave(key, viewer);
302
+ return false;
303
+ }
304
+ let valid = false;
305
+ try {
306
+ valid = viewer.valid();
307
+ }
308
+ catch { /* A failed proof releases only this room. */ }
309
+ if (!valid || event === "gone") {
310
+ // A failed proof may send only the generic departure, never the pending room data.
236
311
  try {
237
- ok = viewer.valid();
312
+ viewer.connection.send(viewer.room, "gone");
238
313
  }
239
- catch {
240
- ok = false;
314
+ finally {
315
+ leave(key, viewer);
241
316
  }
242
- if (!ok)
243
- drop(key, viewer);
317
+ return false;
244
318
  }
319
+ if (event !== undefined)
320
+ viewer.connection.send(viewer.room, event, data);
321
+ return viewer.connection.open && room.viewers.has(viewer);
322
+ };
323
+ /** Recheck who may still see the room, then whether what it shows moved. */
324
+ const refresh = (key, room, revision) => {
325
+ // A signed-out or narrowed account stops hearing about it, at the first write after.
326
+ for (const viewer of [...room.viewers.keys()])
327
+ deliver(key, room, viewer);
245
328
  if (rooms.get(key) !== room)
246
329
  return;
247
330
  let now;
@@ -259,25 +342,28 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
259
342
  }
260
343
  if (now === null) {
261
344
  for (const viewer of [...room.viewers.keys()])
262
- drop(key, viewer);
345
+ deliver(key, room, viewer, "gone");
263
346
  return;
264
347
  }
265
348
  if (now === room.last)
266
349
  return;
267
350
  room.last = now;
268
- for (const stream of room.viewers.values())
269
- stream.send("change", { at: now, revision: revision ?? bus?.revision() ?? null });
270
- };
271
- const tick = (key, room) => {
272
- room.ticks += 1;
273
- if (room.ticks % checkEvery === 0) {
274
- const before = room.last;
275
- refresh(key, room, null);
276
- if (rooms.get(key) === room && room.last !== before)
277
- return;
351
+ room.revision = revision ?? bus?.revision() ?? null;
352
+ if (room.holding !== null)
353
+ return;
354
+ const wait = room.saidAt + minIntervalMs - Date.now();
355
+ if (wait > 0) {
356
+ room.holding = setTimeout(() => { room.holding = null; if (rooms.get(key) === room)
357
+ say(key, room); }, wait);
358
+ room.holding.unref();
359
+ return;
278
360
  }
279
- for (const stream of room.viewers.values())
280
- stream.keepAlive();
361
+ say(key, room);
362
+ };
363
+ const say = (key, room) => {
364
+ room.saidAt = Date.now();
365
+ for (const viewer of [...room.viewers.keys()])
366
+ deliver(key, room, viewer, "change", { at: room.last, revision: room.revision });
281
367
  };
282
368
  const heard = (change) => {
283
369
  for (const [key, room] of [...rooms])
@@ -292,24 +378,21 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
292
378
  last = fingerprint(key);
293
379
  }
294
380
  catch { /* the next signal or the safety net reads it */ }
295
- const created = { viewers: new Map(), last, timer: setInterval(() => tick(key, created), keepAliveMs), ticks: 0, settling: null, retry: null };
381
+ const created = { viewers: new Map(), last, timer: setInterval(() => refresh(key, created, null), safetyNetMs), settling: null, retry: null, saidAt: 0, holding: null, revision: null };
296
382
  created.timer.unref();
297
383
  room = created;
298
384
  rooms.set(key, room);
299
385
  if (bus !== null && unsubscribe === null)
300
386
  unsubscribe = bus.subscribe(heard);
301
387
  }
302
- // Remove an overflowed page immediately, even while its final bytes
303
- // wait for the client. Peers must not keep seeing stale presence.
304
- const stream = new LiveStream(viewer.response, options.limitBytes, () => leave(key, viewer));
305
- room.viewers.set(viewer, stream);
306
- viewer.response.once("close", () => leave(key, viewer));
307
- stream.send("change", { at: room.last, revision: bus?.revision() ?? null });
308
- if (!stream.open)
388
+ const joined = room;
389
+ const forget = viewer.connection.onClose(() => leave(key, viewer));
390
+ const unprove = viewer.connection.onKeepAlive(() => { deliver(key, joined, viewer); });
391
+ room.viewers.set(viewer, () => { forget(); unprove(); });
392
+ if (!deliver(key, room, viewer, "change", { at: room.last, revision: bus?.revision() ?? null }) || presence === null)
309
393
  return;
310
- stream.send("here", { people: here(room, viewer) });
311
- if (stream.open)
312
- announce(room);
394
+ if (deliver(key, room, viewer, "here", { people: presence([...room.viewers.keys()], viewer.name) }))
395
+ announce(key, room);
313
396
  },
314
397
  close() {
315
398
  for (const [key, room] of [...rooms])
@@ -159,7 +159,7 @@ export declare function finishConnect(store: Store, visit: ConnectVisit, code: s
159
159
  ok: false;
160
160
  said: string;
161
161
  }>;
162
- /** Keep connected services signed in: any token expiring within ten minutes is refreshed (the worker's pass, and before a teammate's call). */
162
+ /** Keep connected services signed in: any token expiring within ten minutes is refreshed (the worker's pass, and before a subagent's call). */
163
163
  export declare function refreshConnections(store: Store, repos: readonly string[], now: Date, options?: {
164
164
  fetcher?: Fetch;
165
165
  home?: string;
@@ -8,7 +8,7 @@
8
8
  * with this console's callback, and sends the person to the service to sign
9
9
  * in. The callback trades the code for tokens, which live in the tool's own
10
10
  * secrets file (never the database, never a log), and the tool joins the
11
- * project like any other: tested, listed, and given to teammates under rules.
11
+ * project like any other: tested, listed, and given to subagents under rules.
12
12
  * The worker refreshes tokens before they expire.
13
13
  */
14
14
  import { createHash, randomBytes } from "node:crypto";
@@ -382,10 +382,10 @@ export async function finishConnect(store, visit, code, now, options = {}) {
382
382
  if (options.test === false)
383
383
  return { ok: true, said: `${service.label} is connected to ${to}.` };
384
384
  const tested = await testToolOf(store, visit.repo, spec.name, now, { home, ...(options.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) });
385
- return tested?.ok ? { ok: true, said: `${service.label} is connected to ${to}: ${tested.tools.length} action${tested.tools.length === 1 ? "" : "s"}. Let a teammate use it from its page.` }
385
+ return tested?.ok ? { ok: true, said: `${service.label} is connected to ${to}: ${tested.tools.length} action${tested.tools.length === 1 ? "" : "s"}. Let a subagent use it from its page.` }
386
386
  : { ok: true, said: `${service.label} is signed in for ${to}, but its test didn't pass yet: ${tested?.problem ?? "no answer"}.` };
387
387
  }
388
- /** Keep connected services signed in: any token expiring within ten minutes is refreshed (the worker's pass, and before a teammate's call). */
388
+ /** Keep connected services signed in: any token expiring within ten minutes is refreshed (the worker's pass, and before a subagent's call). */
389
389
  export async function refreshConnections(store, repos, now, options = {}) {
390
390
  const fetcher = options.fetcher ?? fetch, home = options.home ?? homedir();
391
391
  const report = { refreshed: 0, problems: [] };
package/dist/mcp-core.js CHANGED
@@ -17,7 +17,7 @@ import { GATEWAY_TOOL_INPUTS, GATEWAY_TOOL_OUTPUTS } from "./contracts/gateway-t
17
17
  import { reportToolOutput } from "./contracts/lead-tools.js";
18
18
  import { PACKAGE_VERSION } from "./version.js";
19
19
  import { authenticateCoordinator, fileCoordinatorProposal, statusFor, listTasksFor, taskDetailFor, } from "./coordinator.js";
20
- import { ISO_STAMP_RULE, decisionOver, decisionsOver, labelRepos, queueOver, recapOver } from "./mate-tools.js";
20
+ import { ISO_STAMP_RULE, decisionOver, decisionsOver, labelRepos, queueOver, recapOver } from "./lead-tools.js";
21
21
  import { proposeAsCoordinator } from "./coordinator-proposals.js";
22
22
  export const MODERN = "2026-07-28";
23
23
  export const LEGACY = "2025-11-25";