toolroll 0.9.63 → 0.9.65

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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -1,3 +1,4 @@
1
+ import { freshFacts, freshlyApprovable, freshnessFor, spendingOnlyOnSuccess, withFreshness } from "../scope.js";
1
2
  import { handlersOf } from './handler-registry.js';
2
3
  import { DEPRECATED_PAGE } from "../deprecations.js";
3
4
  /** chat handlers, moved without changing their route bodies. */
@@ -34,9 +35,9 @@ import { runActivityOf } from "../task-activity.js";
34
35
  import { teamWorkspaceHtml } from '../team-ui.js';
35
36
  import { loadBotToken, redactToken } from "../telegram.js";
36
37
  import { WORK_INDEX_MAX_LIMIT, workIndexPage } from "../work-index.js";
37
- import { html, htmlString, postForm } from "../html.js";
38
+ import { html, htmlString, postForm, reauth } from "../html.js";
38
39
  import { requestContext } from "./request-context.js";
39
- import { refuse, screen } from "./chrome.js";
40
+ import { refuse, screen, whenTime } from "./chrome.js";
40
41
  import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
41
42
  import { chatAckPage, chatPage, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
42
43
  import {} from "./render-pages.js";
@@ -60,17 +61,18 @@ export function createChatHandlers(runtime) {
60
61
  }
61
62
  store.sweepStaleLeadTurns(now);
62
63
  const request = url.searchParams.get("request") ?? "";
63
- const receipt = /^[a-f0-9]{32}$/.test(request) ? store.leadRequestReceipt(session.id, request) : null;
64
+ // Received: the engine took it, or the lead queue holds it (a queued acknowledgement lost on the way still frees the draft).
65
+ const received = /^[a-f0-9]{32}$/.test(request) && store.leadRequestReceived(session.id, request);
64
66
  const requestedTask = url.searchParams.get("task");
65
67
  const focusTask = requestedTask === null || requestedTask === "" ? null : taskChatFocus(requestedTask, now, who, { mintNonce: false });
66
68
  if (requestedTask !== null && requestedTask !== "" && focusTask === null) {
67
69
  // The lens's task is gone or no longer admitted: the page keeps its
68
70
  // draft and says so; it never falls back to the unified thread.
69
- return respond(response, 200, "application/json", JSON.stringify({ session: session.id, task: requestedTask, unavailable: true, received: receipt !== null }));
71
+ return respond(response, 200, "application/json", JSON.stringify({ session: session.id, task: requestedTask, unavailable: true, received }));
70
72
  }
71
73
  const chatProject = focusTask !== null ? null : chatProjectOf(url.searchParams.getAll("project").filter(one => one !== ""));
72
74
  if (chatProject === undefined)
73
- return respond(response, 200, "application/json", JSON.stringify({ session: session.id, unavailable: true, received: receipt !== null }));
75
+ return respond(response, 200, "application/json", JSON.stringify({ session: session.id, unavailable: true, received }));
74
76
  const rows = leadConversationRows(who, principal, focusTask, now, chatProject);
75
77
  const version = leadChatVersion({ ...rows, focusTask });
76
78
  const known = url.searchParams.get("version") ?? "";
@@ -87,7 +89,7 @@ export function createChatHandlers(runtime) {
87
89
  task: focusTask?.id ?? "",
88
90
  version,
89
91
  pending: rows.pending !== null,
90
- received: receipt !== null,
92
+ received,
91
93
  approval: focusTask?.approval?.digest ?? "",
92
94
  ...(fragments === null ? {} : { fragments }),
93
95
  }));
@@ -357,7 +359,7 @@ export function createChatHandlers(runtime) {
357
359
  // The one nonce in chat — this screen restates exact financial terms
358
360
  // and re-enables spend, which is precisely what nonces are for.
359
361
  const nonce = mintApprovalNonce(who.name, `chat-ack-${turn.id}`, String(turn.reservedMicrousd));
360
- return sendScreen(response, 200, chatAckPage(chromeFor(project, "chat"), turn, nonce));
362
+ return sendScreen(response, 200, chatAckPage(chromeFor(project, "chat"), turn, nonce, store.leadTurnOfStep(turn.id)));
361
363
  }
362
364
  async function sharedActionReview(ctx) {
363
365
  const { url, response } = ctx;
@@ -391,7 +393,22 @@ export function createChatHandlers(runtime) {
391
393
  const review = mintSharedActionReview(store, principal, id, evidenceRoot, clock()), action = review.payload;
392
394
  const terms = action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`);
393
395
  const evidenceLink = action.operation === 'result_accept' ? html `<a href="/r/${Number(action.request['run'])}">Inspect this result</a>` : '';
394
- const content = html `<section class="shared-action"><h1>${action.title}</h1>${evidenceLink}${terms}${postForm(`/chat/proposal/${id}/confirm`, html `<label class="arm"><input type="checkbox" name="confirm" value="yes" required>I confirm this exact action</label>${CHAT_ACTIONS[action.operation].password ? html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>` : ''}<button>${CHAT_ACTIONS[action.operation].label}</button>`, { returnTo: '/chat', hidden: { nonce: review.nonce } })}</section>`;
396
+ // Cockpit B2: approving standard work may stand on a password remembered on this session, exactly as on the task
397
+ // page (the same rule approve() applies when the form comes back); the field then may be left empty.
398
+ const freshSince = (() => {
399
+ const task = typeof action.request['task'] === 'string' ? action.request['task'] : null;
400
+ if (action.operation !== 'scope_approve' || task === null)
401
+ return null;
402
+ const scope = store.getScope(task), fresh = withFreshness('approve-standard', () => freshnessFor(who.name, Date.now(), true));
403
+ if (scope === null || fresh === null)
404
+ return null;
405
+ const gate = store.approvalGate(task, who.name, 'person');
406
+ return freshlyApprovable(store, task, scope, gate.verdict === 'seal' && !gate.protectedWork) ? new Date(fresh.at) : null;
407
+ })();
408
+ const passwordField = !CHAT_ACTIONS[action.operation].password ? '' : reauth(freshSince === null
409
+ ? html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`
410
+ : html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label><p class="approval-password-note" data-approval-fresh>Your password from ${whenTime(freshSince.toISOString())} is remembered on this computer — leave this blank to approve with it, or type it again.</p>`);
411
+ const content = html `<section class="shared-action"><h1>${action.title}</h1>${evidenceLink}${terms}${postForm(`/chat/proposal/${id}/confirm`, html `<label class="arm"><input type="checkbox" name="confirm" value="yes" required>I confirm this exact action</label>${passwordField}<button>${CHAT_ACTIONS[action.operation].label}</button>`, { returnTo: '/chat', hidden: { nonce: review.nonce } })}</section>`;
395
412
  return sendScreen(response, 200, screen('Review action', content, { chrome: chromeFor(action.repo, 'chat') }));
396
413
  }
397
414
  catch (error) {
@@ -665,6 +682,22 @@ export function createChatHandlers(runtime) {
665
682
  leadSaid.delete(who.session.csrf);
666
683
  return redirect(response, back);
667
684
  }
685
+ /** Remove a message still waiting in the person's lead queue: theirs only, only while it waits; its words are forgotten. */
686
+ async function leadQueuedRemove(ctx) {
687
+ const { url, who, request, response, now, posted } = ctx;
688
+ const body = readForm(posted, CONSOLE_FORMS.leadQueuedRemove);
689
+ if (who.via !== "cookie")
690
+ return refuse(response, who, 403, "the lead is a browser surface");
691
+ const wantsJson = String(request.headers["accept"] ?? "").includes("application/json");
692
+ const back = safeChatReturn(body.get("return"));
693
+ const outcome = runtime.personalQueue.queue.remove(who.name, Number(url.pathname.split("/")[3]), now);
694
+ const words = outcome === "removed" ? "Removed. It won't be sent." : outcome === "started" ? "Already sent: it started before it could be removed." : "That message isn't waiting.";
695
+ if (wantsJson)
696
+ return respond(response, outcome === "not-found" ? 404 : outcome === "started" ? 409 : 200, "application/json", JSON.stringify({ ok: outcome === "removed", said: words }));
697
+ if (outcome === "not-found")
698
+ return refuse(response, who, 404, words, back);
699
+ return redirect(response, chatReturnWithSaid(back, words));
700
+ }
668
701
  async function leadStop(ctx) {
669
702
  const { who, response, now, posted } = ctx;
670
703
  const body = readForm(posted, CONSOLE_FORMS.leadStop);
@@ -722,7 +755,12 @@ export function createChatHandlers(runtime) {
722
755
  noteLead(who.session.csrf, null, "that proposal was already acted on");
723
756
  return redirect(response, chatReturnWithLatest(back));
724
757
  }
725
- const outcome = confirmLeadProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
758
+ // A scope approval's re-check is spent only if the whole confirmation commits (one transaction: the proposal's
759
+ // settlement included). Any other action spends it whatever comes of it (its effects aren't all in the database).
760
+ const confirmIt = () => confirmLeadProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
761
+ const proposed = store.getLeadProposal(id);
762
+ const approving = proposed?.kind === "action" && sharedActionPayload(proposed.payload)?.operation === "scope_approve";
763
+ const outcome = approving ? spendingOnlyOnSuccess(confirmIt, result => !result.ok || ("already" in result && result.already === true)) : confirmIt();
726
764
  if (cardJson) {
727
765
  if (!outcome.ok)
728
766
  return cardAnswer(outcome.reason === "standing" ? 403 : outcome.reason === "not-yours" ? 404 : 409, false, outcome.said);
@@ -839,10 +877,12 @@ export function createChatHandlers(runtime) {
839
877
  // its own terms and the thread shows why.
840
878
  const leadSession = who.role === "approver" ? store.activeLeadSession(who.name) : null;
841
879
  if (leadSession !== null) {
842
- const requestId = body.get("request");
843
- if (requestId !== null && (body.getAll("request").length !== 1 || body.getAll("request-session").length !== 1 || body.get("request-session") !== String(leadSession.id))) {
880
+ const sentRequest = body.get("request");
881
+ if (sentRequest !== null && (body.getAll("request").length !== 1 || body.getAll("request-session").length !== 1 || body.get("request-session") !== String(leadSession.id))) {
844
882
  return said(409, "This conversation changed. Reload it before sending your message.");
845
883
  }
884
+ // Every send has one identity (a form without one gets it here): a message that waits is known by it.
885
+ const requestId = sentRequest ?? randomBytes(16).toString("hex");
846
886
  const principal = leadPrincipal(who);
847
887
  if (principal === null)
848
888
  return wantsJson ? said(403, "your approver standing changed — sign in again") : refuse(response, who, 403, "your approver standing changed — sign in again", back);
@@ -851,15 +891,50 @@ export function createChatHandlers(runtime) {
851
891
  return said(400, `a message is 1 to ${LEAD_MESSAGE_MAX_CHARS} characters`, leadSession.id);
852
892
  }
853
893
  const opened = store.openLeadThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
854
- const onProgress = beginLiveTurn(opened.thread.id);
855
- void runLeadTurn({ store, who: principal, session: leadSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, channel: "console", ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
894
+ // The personal queue (docs/plans/lead-message-queue.md): while this person's lead is busy, or something of theirs
895
+ // waits, the message waits its turn instead of being refused. A request the engine already took replays as before.
896
+ const queueContext = focusTask !== null
897
+ ? { task: focusTask.id, ...(viewedRun === null ? {} : { result: viewedRun.id }), ...(modeValue === null ? {} : { mode: modeValue }) }
898
+ : chatProject !== null ? { project: chatProject } : null;
899
+ const queueIt = () => runtime.personalQueue.queue.sendOrQueue({ approver: who.name, approverGeneration: principal.generation, session: leadSession.id,
900
+ ceilingDigest: principal.ceilingDigest, thread: opened.thread.id, text: message, requestId, context: queueContext }, clock());
901
+ // A message the engine would refuse for a secret never waits (nothing stored): it goes straight to the engine,
902
+ // which refuses it before any row exists, exactly as when the lead is free.
903
+ if (scanForSecrets(message).length === 0 && store.leadRequestReceipt(leadSession.id, requestId) === null) {
904
+ const decided = queueIt();
905
+ if (decided.kind === "conflict")
906
+ return said(409, "This message was already sent with different words. Reload the conversation to see it.", leadSession.id);
907
+ if (decided.kind === "full")
908
+ return said(409, decided.message, leadSession.id);
909
+ if (decided.kind === "queued") {
910
+ runtime.personalQueue.kick();
911
+ return wantsJson
912
+ ? respond(response, 202, "application/json", JSON.stringify({ ok: true, queued: true, session: leadSession.id, task: focusTask?.id ?? "", ...(chatProject === null ? {} : { project: chatProject }), request: requestId, message: decided.row.message, status: decided.row.status }))
913
+ : redirect(response, chatReturnWithLatest(back));
914
+ }
915
+ }
916
+ // The live display starts once this message holds a turn: a refusal (a secret, a turn still running) never blanks
917
+ // the display of a reply that is still running in the same conversation.
918
+ let onProgress;
919
+ const ended = (ok) => { if (onProgress !== undefined)
920
+ endLiveTurn(opened.thread.id, ok, onProgress); };
921
+ // Detached work never carries this request's browser session (Passwordless reads it): it runs with no session facts.
922
+ void freshFacts.run(null, () => runLeadTurn({ store, who: principal, session: leadSession, thread: opened.thread, config: enabled.config, key: enabled.key, message,
923
+ onAdmitted: () => { onProgress = beginLiveTurn(opened.thread.id); }, onProgress: event => onProgress?.(event), channel: "console", requestId, ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot }))
856
924
  .then(outcome => {
925
+ // Lost a race to another turn of this person's between deciding and admission: it waits instead of being refused.
926
+ if (!outcome.ok && "refused" in outcome && (outcome.refused === "concurrent" || outcome.refused === "backlog") && queueIt().kind === "queued") {
927
+ ended(false);
928
+ return;
929
+ }
857
930
  // A turn that saved its outcome in the thread (one stopped at its deadline) is said there, once.
858
931
  if (!outcome.ok && !("saved" in outcome && outcome.saved === true))
859
932
  noteLead(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
860
- endLiveTurn(opened.thread.id, outcome.ok);
933
+ ended(outcome.ok);
861
934
  })
862
- .catch(() => { noteLead(who.session.csrf, null, "the turn failed unexpectedly"); endLiveTurn(opened.thread.id, false); });
935
+ .catch(() => { noteLead(who.session.csrf, null, "the turn failed unexpectedly"); ended(false); })
936
+ // A personal turn ended: the next waiting message (this person's or anyone's) may start now.
937
+ .finally(() => runtime.personalQueue.kick());
863
938
  // Accepted for the engine, not received: the receipt is written by
864
939
  // the turn's own admission, and the status poll is where the page
865
940
  // learns of it — the same road as after a native send.
@@ -877,10 +952,6 @@ export function createChatHandlers(runtime) {
877
952
  }
878
953
  // The password, typed again, on EVERY message (v2 ruling 2): chat is
879
954
  // spend, and a seven-day cookie is not a spend credential.
880
- const password = body.get("token") ?? "";
881
- if (password === "" || !authenticateApprover(who, password).ok) {
882
- return redirect(response, `/chat?said=${encodeURIComponent("chat spends — your password, typed again, with every message")}`);
883
- }
884
955
  const message = (body.get("message") ?? "").trim();
885
956
  if (message === "" || message.length > 2_000) {
886
957
  return redirect(response, `/chat?said=${encodeURIComponent("a message is 1 to 2000 characters")}`);
@@ -912,16 +983,28 @@ export function createChatHandlers(runtime) {
912
983
  if (disallowed !== null)
913
984
  return redirect(response, `/chat?said=${encodeURIComponent(disallowed)}`);
914
985
  const reserved = worstCaseForPrice(enabled.price, Buffer.byteLength(composed.body, "utf8"));
915
- const opened = store.openChatTurn({
916
- approver: who.name,
917
- credentialKey: enabled.credentialKey,
918
- provider: enabled.config.provider,
919
- model: enabled.config.model,
920
- reservedMicrousd: reserved,
921
- dailyTurns: enabled.config.dailyTurns,
922
- weeklyCeilingMicrousd: enabled.config.weeklyCeilingMicrousd,
923
- deadlineMs: TURN_WALL_CLOCK_MS + 10_000,
924
- }, now);
986
+ // The password comes last, right where the turn opens: every refusal above leaves an explicit re-check unspent,
987
+ // and a turn that doesn't open (a cap, a budget) puts it back.
988
+ const password = body.get("token") ?? "";
989
+ const opened = spendingOnlyOnSuccess(() => {
990
+ // Typed now, or (cockpit B2) a password or explicit provider re-check given on this session, on a direct local
991
+ // request, within its window. An ordinary provider sign-in never starts that window.
992
+ if (!withFreshness("chat-send", () => authenticateApprover(who, password)).ok)
993
+ return null;
994
+ return store.openChatTurn({
995
+ approver: who.name,
996
+ credentialKey: enabled.credentialKey,
997
+ provider: enabled.config.provider,
998
+ model: enabled.config.model,
999
+ reservedMicrousd: reserved,
1000
+ dailyTurns: enabled.config.dailyTurns,
1001
+ weeklyCeilingMicrousd: enabled.config.weeklyCeilingMicrousd,
1002
+ deadlineMs: TURN_WALL_CLOCK_MS + 10_000,
1003
+ }, now);
1004
+ }, result => result === null || !result.ok);
1005
+ if (opened === null) {
1006
+ return redirect(response, `/chat?said=${encodeURIComponent("chat spends — your password, typed again, with every message")}`);
1007
+ }
925
1008
  if (!opened.ok) {
926
1009
  const said = opened.reason === "latched"
927
1010
  ? "a turn with unknown cost blocks this credential — acknowledge it below first"
@@ -1046,6 +1129,9 @@ export function createChatHandlers(runtime) {
1046
1129
  const wanted = values[0] ?? "";
1047
1130
  return values.length === 1 && visible(wanted) && managedRepos().includes(wanted) ? wanted : undefined;
1048
1131
  }
1132
+ /** Which live display each progress handle started: ending a turn ends only its own display (a turn stopped from
1133
+ * another window that settles late can never end the next turn's). */
1134
+ const progressOf = new WeakMap();
1049
1135
  function beginLiveTurn(thread) {
1050
1136
  const previous = liveTurns.get(thread);
1051
1137
  if (previous?.expiry)
@@ -1055,7 +1141,7 @@ export function createChatHandlers(runtime) {
1055
1141
  // A page that has this conversation open follows the reply (GET /live, its chat room).
1056
1142
  runtime.liveTurnStarted(thread);
1057
1143
  const current = () => live.steps.at(-1) ?? (live.steps.push({ tools: [], toolCalls: [], text: "" }), live.steps.at(-1));
1058
- return event => {
1144
+ const progress = (event) => {
1059
1145
  if (live.done)
1060
1146
  return;
1061
1147
  if (event.kind === "step")
@@ -1078,11 +1164,15 @@ export function createChatHandlers(runtime) {
1078
1164
  for (const listener of live.listeners)
1079
1165
  listener();
1080
1166
  };
1167
+ progressOf.set(progress, live);
1168
+ return progress;
1081
1169
  }
1082
- function endLiveTurn(thread, ok) {
1170
+ function endLiveTurn(thread, ok, progress) {
1083
1171
  const live = liveTurns.get(thread);
1084
1172
  if (live === undefined || live.done)
1085
1173
  return;
1174
+ if (progress !== undefined && progressOf.get(progress) !== live)
1175
+ return;
1086
1176
  live.done = true;
1087
1177
  live.ok = ok;
1088
1178
  for (const listener of live.listeners)
@@ -1388,6 +1478,7 @@ export function createChatHandlers(runtime) {
1388
1478
  "chat.mate-follow": leadFollow,
1389
1479
  "chat.mate-end": leadEnd,
1390
1480
  "chat.mate-stop": leadStop,
1481
+ "chat.queued-remove": leadQueuedRemove,
1391
1482
  "chat.proposal.confirm": chatProposalAct("confirm"),
1392
1483
  "chat.proposal.dismiss": chatProposalAct("dismiss"),
1393
1484
  "coordinator.proposal.confirm": coordinatorProposalAct("confirm"),
@@ -134,7 +134,8 @@ export declare function regionScript(regionId: string, fragmentName: string, pat
134
134
  * protocol, not an HTML fragment — the response is raw sanitized text and
135
135
  * the ONLY sink is textContent, so nothing here can become markup. One
136
136
  * request in flight, visibility-paused, stops on `final`, and a `replaced`
137
- * answer restarts from zero with a visible line — never a silent re-read.
137
+ * answer — or another file's generation — restarts from zero with a visible
138
+ * line, never a silent re-read.
138
139
  */
139
140
  export declare function transcriptScript(path?: string, elementId?: string): string;
140
141
  /**
@@ -170,6 +171,10 @@ export type Screen = {
170
171
  /** Render sensitive even when no password field is visible — one-time
171
172
  * secrets and judgment calls the classifier cannot see. */
172
173
  forceSensitive?: boolean;
174
+ /** The page's sensitivity, said explicitly (cockpit §2, R14): true for a page that asks for a password or shows a
175
+ * secret even when no password field renders (an SSO step-up, a fresh-password grant); false only where a page is
176
+ * known safe. Unset: judged from the page as authored. A sensitive page has no sidebar states and no summary room. */
177
+ sensitive?: boolean;
173
178
  /** Structured conversation; complex guarded forms stay native islands. */
174
179
  workspace?: Partial<Pick<BrowserWorkspace, 'conversation' | 'team' | 'focus' | 'result' | 'notices' | 'view' | 'firstRun' | 'phone' | 'home'>> & {
175
180
  /** Markup the workspace shows in place; it becomes bytes where the page's JSON is written (sendScreen). */
@@ -223,9 +223,17 @@ export function regionScript(regionId, fragmentName, path) {
223
223
  // A fragment that marks itself final stops: a finished or abandoned build is not fetched again.
224
224
  `.then(function(){busy=false;tell();if(region.querySelector("[data-region-stop]")){stopped=true;if(stamp)stamp.textContent="";return;}if(again)refresh();});}` +
225
225
  `function heard(name){if(name==="change"||name==="reload")refresh();}` +
226
- `if(document.documentElement.dataset.live==="1"){addEventListener("so:live",function(e){heard(e.detail&&e.detail.event);});}` +
227
- `else if(typeof EventSource!=="undefined"){var es=new EventSource("/live?room=workspace");` +
226
+ // On a workspace page the page's one stream (browser/live.ts) carries the workspace room: ask it, never open a second.
227
+ `function mine(e){var d=e.detail;if(d&&(d.room==="workspace"||d.room===null))heard(d.event);}` +
228
+ `if(document.getElementById("standing-orders-workspace")){(window.__soLiveRooms=window.__soLiveRooms||[]).push("workspace");addEventListener("so:live",mine);}` +
229
+ `else if(document.documentElement.dataset.live==="1"){addEventListener("so:live",mine);}` +
230
+ // Refused (signed out, access lost): stop instead of reconnecting at once into a request that can't sign in, and
231
+ // listen again in 30 s, as the page's own stream does (browser/live.ts RETRY_MS).
232
+ `else if(typeof EventSource!=="undefined"){var listen=function(){var es=new EventSource("/live?room=workspace");` +
228
233
  `es.addEventListener("change",function(){heard("change");});es.addEventListener("reload",function(){heard("reload");});` +
234
+ // A retry the server answers with an error (still signed out) is given up by the browser: try again later too.
235
+ `var again=function(){es.close();setTimeout(listen,30000);};` +
236
+ `es.addEventListener("ended",again);es.addEventListener("error",function(){if(es.readyState===2)again();});};listen();` +
229
237
  `document.addEventListener("visibilitychange",function(){if(!document.hidden)refresh();});}` +
230
238
  `})();`);
231
239
  }
@@ -234,23 +242,24 @@ export function regionScript(regionId, fragmentName, path) {
234
242
  * protocol, not an HTML fragment — the response is raw sanitized text and
235
243
  * the ONLY sink is textContent, so nothing here can become markup. One
236
244
  * request in flight, visibility-paused, stops on `final`, and a `replaced`
237
- * answer restarts from zero with a visible line — never a silent re-read.
245
+ * answer — or another file's generation — restarts from zero with a visible
246
+ * line, never a silent re-read.
238
247
  */
239
248
  export function transcriptScript(path, elementId = "live-transcript") {
240
249
  const base = path === undefined ? "location.pathname" : JSON.stringify(path);
241
250
  return (`(function(){var out=document.getElementById(${JSON.stringify(elementId)});if(!out)return;` +
242
- `var from=0,busy=false,stopped=false;` +
251
+ `var from=0,gen="",inkey=false,ktail="",busy=false,stopped=false;` +
243
252
  `function near(){return out.scrollHeight-out.scrollTop-out.clientHeight<40;}` +
244
253
  `function go(){if(stopped)return;if(busy||document.hidden){later();return;}busy=true;` +
245
- `fetch(${base}+"?fragment=transcript&from="+from,{redirect:"manual",cache:"no-store"})` +
254
+ `fetch(${base}+"?fragment=transcript&from="+from+(gen?"&gen="+encodeURIComponent(gen):"")+(inkey?"&inkey=1":"")+(ktail?"&ktail="+encodeURIComponent(ktail):""),{redirect:"manual",cache:"no-store"})` +
246
255
  `.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){stopped=true;return null;}` +
247
256
  `if(r.status===409)return{error:"replaced"};return r.ok?r.json():null;})` +
248
257
  `.then(function(d){busy=false;if(d===null){later();return;}if(stopped)return;` +
249
- `if(d.error==="replaced"){from=0;out.textContent="[the view restarted]\\n";later();return;}` +
258
+ `if(d.error==="replaced"){from=0;gen="";inkey=false;ktail="";out.textContent="[the view restarted]\\n";later();return;}` +
250
259
  `if(d.error){stopped=true;return;}` +
251
260
  `var stick=near();` +
252
261
  `if(typeof d.text==="string"&&d.text!==""){out.appendChild(document.createTextNode(d.text));if(stick)out.scrollTop=out.scrollHeight;}` +
253
- `if(typeof d.nextOffset==="number"&&d.nextOffset>=from){from=d.nextOffset;}` +
262
+ `if(typeof d.nextOffset==="number"&&d.nextOffset>=from){from=d.nextOffset;}if(typeof d.gen==="string")gen=d.gen;if(typeof d.inKey==="boolean")inkey=d.inKey;if(typeof d.keyTail==="string")ktail=d.keyTail;` +
254
263
  `if(d.final===true){stopped=true;var m=document.getElementById(${JSON.stringify(`${elementId}-state`)});` +
255
264
  `if(m)m.textContent="the agent finished — the record on this page is the story";return;}` +
256
265
  `later();})` +
@@ -680,6 +689,8 @@ export const NAV_ICONS = {
680
689
  export function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
681
690
  const rows = [
682
691
  // D5: coding sessions are deprecated; /code still opens this release, but nothing links to it.
692
+ // Live terminals: an operator's own Claude Code, Codex or shell, in a real terminal (docs/plans/live-terminal.md).
693
+ ...(offersCode ? [{ key: "code", href: "/term", label: "Live", hint: "your own terminals: Claude Code, Codex or a shell" }] : []),
683
694
  { key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
684
695
  { key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
685
696
  { key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
@@ -182,7 +182,7 @@
182
182
  the act that resolves the screen. Cards, frames, and seals are neutral. */
183
183
  .badge-open, .badge-parked { color: var(--foreground); }
184
184
  .count {
185
- min-width: 1.25rem; padding-inline: .35rem; border-radius: 9999px;
185
+ min-width: 1.25rem; padding-inline: .35rem; border-radius: var(--so-radius-full);
186
186
  }
187
187
  .count.badge-open {
188
188
  background: var(--brand-soft); color: var(--brand);
@@ -627,7 +627,7 @@
627
627
  .requirement-heading .badge { margin: 0; white-space: normal; }
628
628
  .requirement-statement { margin: .5rem 0; line-height: 1.55; white-space: pre-wrap; }
629
629
  .requirement-review { margin: .4rem 0; font-size: .78rem; color: var(--muted-foreground); }
630
- .requirement-warning { margin: .65rem 0; padding: .6rem .75rem; border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border)); border-radius: 8px; background: var(--warning-soft); font-size: .8125rem; }
630
+ .requirement-warning { margin: .65rem 0; padding: .6rem .75rem; border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border)); border-radius: var(--so-radius-md); background: var(--warning-soft); font-size: .8125rem; }
631
631
  .requirement-warning strong { font-size: .78rem; }
632
632
  .requirement-warning p { margin: .3rem 0 0; }
633
633
  .requirement-issues { font-size: .8125rem; }
@@ -663,7 +663,7 @@
663
663
 
664
664
  .filters { font-size: 0.8125rem; color: var(--muted-foreground); }
665
665
  .filters a, .filters strong {
666
- display: inline-block; padding: .25rem .625rem; border-radius: 9999px; text-decoration: none;
666
+ display: inline-block; padding: .25rem .625rem; border-radius: var(--so-radius-full); text-decoration: none;
667
667
  color: var(--muted-foreground); font-weight: 500;
668
668
  }
669
669
  .filters strong { background: var(--muted); color: var(--foreground); }
@@ -884,13 +884,18 @@
884
884
  }
885
885
  .banner .badge { margin-right: .5rem; }
886
886
  /* Chat in `toolroll demo`: the scripted lead's conversation. */
887
- .demo-chat { display: flex; flex-direction: column; gap: 1rem; max-width: 46rem; margin: 0 auto; padding-bottom: 1rem; }
887
+ .demo-chat { display: flex; flex-direction: column; gap: 1rem; max-width: 46rem; min-height: 100%; margin: 0 auto; }
888
+ .demo-thread { flex: 1 0 auto; }
889
+ /* Empty: the greeting, the suggestions and the composer sit together in the middle, as in any modern chat. */
890
+ .demo-chat--empty { justify-content: center; min-height: min(70vh, 40rem); }
891
+ .demo-chat--empty .demo-thread { flex: 0 0 auto; }
888
892
  .demo-thread { display: flex; flex-direction: column; gap: 1.25rem; }
889
893
  .demo-turn { display: flex; flex-direction: column; gap: .75rem; scroll-margin-top: 1rem; }
890
894
  .demo-said p { margin: 0; }
891
- .demo-you { align-self: flex-end; max-width: min(34rem, 85%); background: var(--so-ink); color: var(--so-paper); border-radius: .875rem .875rem .25rem .875rem; padding: .55rem .85rem; overflow-wrap: anywhere; }
895
+ .demo-you { align-self: flex-end; max-width: min(34rem, 85%); background: var(--so-soft); color: var(--so-ink); border: 1px solid var(--so-line); border-radius: 1rem 1rem .3rem 1rem; padding: .5rem .85rem; overflow-wrap: anywhere; }
892
896
  .demo-lead { max-width: 40rem; }
893
- .demo-who { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin-bottom: .15rem !important; }
897
+ .demo-who { display: flex; align-items: center; gap: .45rem; font-size: .8125rem; font-weight: 600; color: var(--so-ink); margin-bottom: .3rem !important; }
898
+ .demo-avatar { display: inline-grid; place-items: center; width: 1.25rem; height: 1.25rem; border-radius: 50%; background: var(--so-ink); color: var(--so-paper); font-size: .6875rem; font-weight: 700; }
894
899
  .demo-chat .card { margin: 0; padding: 1rem 1.125rem; }
895
900
  .demo-chat .card h2 { font-size: .9375rem; margin: .1rem 0 .4rem; }
896
901
  .demo-chat .card h3 { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin: .75rem 0 .25rem; }
@@ -938,13 +943,22 @@
938
943
  .demo-evidence figure { margin: 0 0 .5rem; }
939
944
  .demo-evidence img { display: block; width: 100%; height: auto; border: 1px solid var(--so-line); border-radius: .5rem; }
940
945
  .demo-evidence figcaption { margin-top: .35rem; }
941
- .demo-hint { padding: 1.25rem 0 .25rem; }
942
- .demo-hint-title { font-size: .9375rem; font-weight: 600; margin: 0 0 .75rem; }
943
- .demo-suggestions { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }
944
- .demo-suggestions button { min-height: 2.5rem; border-radius: 999px; padding: .35rem .95rem; }
945
- .demo-composer { position: sticky; bottom: 0; padding: .75rem 0 .25rem; background: var(--so-paper); flex-direction: row; align-items: flex-end; }
946
- .demo-chat .demo-composer textarea { flex: 1 1 auto; width: auto; min-width: 0; min-height: 2.75rem; }
947
- .demo-chat .demo-composer button { flex: none; width: auto; min-height: 2.75rem; padding-inline: 1.1rem; }
946
+ .demo-hint { text-align: center; padding: 0 0 .5rem; }
947
+ .demo-hint-title { font-size: 1.375rem; font-weight: 600; letter-spacing: -.01em; margin: 0 0 .35rem; }
948
+ .demo-hint-sub { color: var(--so-muted); font-size: .875rem; margin: 0 0 1rem; }
949
+ .demo-suggestions { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; margin: 0; padding: 0; border: 0; background: none; }
950
+ .demo-chat .demo-suggestions button { min-height: 2.25rem; border-radius: 999px; padding: .3rem .9rem; border: 1px solid var(--so-line); background: var(--so-paper); color: var(--so-muted); font-weight: 500; box-shadow: none; }
951
+ @media (hover: hover) and (pointer: fine) { .demo-chat .demo-suggestions button:hover { color: var(--so-ink); border-color: var(--so-input-line); background: var(--so-soft); } }
952
+ /* The composer: one card docked at the bottom, a borderless box that grows, the hint and a round send inside. */
953
+ .demo-composer { position: sticky; bottom: 0; margin: 0; padding: .75rem 0 1rem; border: 0; border-radius: 0; box-shadow: none; background: linear-gradient(to bottom, transparent, var(--so-paper) 1.25rem); }
954
+ .demo-composer-card { display: flex; flex-direction: column; gap: .35rem; padding: .7rem .7rem .55rem .9rem; border: 1px solid var(--so-input-line); border-radius: 1rem; background: var(--so-paper); box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 16px rgba(0,0,0,.04); transition: border-color .12s var(--so-ease-out), box-shadow .12s var(--so-ease-out); }
955
+ .demo-composer-card:focus-within { border-color: var(--so-muted); box-shadow: 0 0 0 3px var(--so-soft); }
956
+ .demo-chat .demo-composer textarea { width: 100%; min-height: 1.6rem; max-height: 12rem; field-sizing: content; padding: .15rem 0; border: 0; border-radius: 0; background: none; box-shadow: none; resize: none; font-size: .9375rem; line-height: 1.55; }
957
+ .demo-chat .demo-composer textarea:focus-visible { outline: 0; }
958
+ .demo-composer-bar { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
959
+ .demo-composer-bar > span { color: var(--so-muted); font-size: .75rem; }
960
+ .demo-chat .demo-composer .demo-send { display: inline-grid; place-items: center; flex: none; width: 2rem; min-height: 2rem; height: 2rem; padding: 0; border-radius: 50%; }
961
+ .demo-composer-card:has(textarea:placeholder-shown) .demo-send { opacity: .35; }
948
962
  .banner a { color: var(--muted-foreground); }
949
963
  .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }
950
964
 
@@ -1190,26 +1204,26 @@
1190
1204
  rows, who builds in one line, one Approve & start beside the password,
1191
1205
  and every other signed term in one Details fold at the end. Sentence
1192
1206
  case, Geist, 12px and up; prose capped at 75ch. */
1193
- .approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
1207
+ .approval-sheet { display: grid; gap: var(--so-space-lg); margin: 0; padding: var(--so-space-xl); border: 1px solid var(--border); border-radius: var(--so-radius-card); background: var(--card); }
1194
1208
  .approval-sheet.approve-form > :not(input) { margin: 0; }
1195
1209
  .approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
1196
1210
  .approval-revision, .approval-note { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
1197
1211
  .approval-note { color: var(--muted-foreground); }
1198
1212
  .approval-rows { display: grid; gap: 14px; margin: 0; }
1199
- .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
1213
+ .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: var(--so-space-lg); }
1200
1214
  .approval-row dt { margin: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
1201
1215
  .approval-row dd { min-width: 0; max-width: 75ch; margin: 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
1202
1216
  .approval-row dd p, .approval-sheet .approval-goal { margin: 0; font-size: 14px; line-height: 1.6; }
1203
- .approval-row dd :is(ol, ul) { display: grid; gap: 4px; margin: 0; padding-left: 1.25rem; }
1217
+ .approval-row dd :is(ol, ul) { display: grid; gap: var(--so-space-xs); margin: 0; padding-left: 1.25rem; }
1204
1218
  .approval-row dd ol + p { margin-top: 6px; }
1205
1219
  .approval-row dd .approval-paths { margin-top: 2px; padding-left: 0; }
1206
1220
  .approval-row dd .approval-paths li { font-size: 13px; }
1207
- .approval-amendment { max-width: 75ch; padding: 12px 14px; border-radius: 8px; background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
1221
+ .approval-amendment { max-width: 75ch; padding: var(--so-space-md) 14px; border-radius: var(--so-radius-md); background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
1208
1222
  .approval-amendment p { margin: 0; }
1209
- .approval-amendment ul { display: grid; gap: 4px; margin: 6px 0; padding-left: 1.25rem; }
1223
+ .approval-amendment ul { display: grid; gap: var(--so-space-xs); margin: 6px 0; padding-left: 1.25rem; }
1210
1224
  .agents-size { margin: 0; font-size: 13px; line-height: 1.5; }
1211
1225
  .approval-who { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
1212
- .approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: 8px; }
1226
+ .approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: var(--so-space-sm); }
1213
1227
  .approval-password { margin: 0; }
1214
1228
  .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
1215
1229
  .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
@@ -1218,7 +1232,7 @@
1218
1232
  .approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }
1219
1233
  .approval-act > button { grid-column: 2; grid-row: 1; }
1220
1234
  .approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
1221
- .approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
1235
+ .approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: var(--so-space-sm); }
1222
1236
  details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
1223
1237
  /* Edit plan, in place: the rows become fields where they stood; the
1224
1238
  approval act and Details step aside until it is saved or cancelled. */
@@ -1233,32 +1247,32 @@
1233
1247
  .approval-secondary:has(> .approval-edit[open]) > a { display: none; }
1234
1248
  .approval-editor { display: grid; gap: 14px; }
1235
1249
  .approval-editor .problem { margin: 0; }
1236
- .approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }
1250
+ .approval-field { display: grid; gap: var(--so-space-xs); min-width: 0; margin: 0; padding: 0; border: 0; }
1237
1251
  .approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
1238
1252
  .approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
1239
1253
  .approval-field textarea, .approval-field input[type=text] { width: 100%; max-width: 75ch; margin: 0; font-family: var(--font-sans); font-size: 14px; line-height: 1.55; }
1240
1254
  .approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
1241
- .approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
1255
+ .approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: var(--so-space-sm); }
1242
1256
  .approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
1243
- .approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }
1257
+ .approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 var(--so-space-md); border: 1px solid var(--border); border-radius: var(--so-radius-md); background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }
1244
1258
  .approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
1245
1259
  @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
1246
1260
  details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
1247
1261
  details.approval-details[open] { padding-bottom: 0; }
1248
- .approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: 4px 0 0; font-size: 13px; }
1249
- .approval-detail { max-width: 75ch; margin: 12px 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
1250
- .approval-detail h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }
1251
- .approval-detail p, .approval-detail ul, .approval-detail dl { margin: 4px 0; }
1262
+ .approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: var(--so-space-xs) 0 0; font-size: 13px; }
1263
+ .approval-detail { max-width: 75ch; margin: var(--so-space-md) 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
1264
+ .approval-detail h3 { margin: 0 0 var(--so-space-xs); font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }
1265
+ .approval-detail p, .approval-detail ul, .approval-detail dl { margin: var(--so-space-xs) 0; }
1252
1266
  .approval-detail ul { padding-left: 1.25rem; }
1253
1267
  .approval-detail .revision-card { margin: 0; padding: 0; border: 0; background: none; }
1254
- .approval-roles { display: grid; gap: 8px; }
1255
- .approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 12px; }
1268
+ .approval-roles { display: grid; gap: var(--so-space-sm); }
1269
+ .approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: var(--so-space-md); }
1256
1270
  .approval-roles dt { color: var(--muted-foreground); font-weight: 500; }
1257
1271
  .approval-roles dd { margin: 0; }
1258
1272
  .approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }
1259
1273
  .chat-plan .approval-sheet { padding: 0; border: 0; background: none; }
1260
1274
  @media (max-width: 760px) {
1261
- .approval-sheet { gap: 12px; padding: 14px; }
1275
+ .approval-sheet { gap: var(--so-space-md); padding: 14px; }
1262
1276
  .approval-row { grid-template-columns: 1fr; gap: 2px; }
1263
1277
  .approval-act { grid-template-columns: 1fr; }
1264
1278
  .approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
@@ -1269,7 +1283,7 @@
1269
1283
  position: sticky; bottom: 0; z-index: 20; margin: 0 -14px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
1270
1284
  border-top: 1px solid var(--border); background: var(--card);
1271
1285
  }
1272
- .approval-link { min-height: 44px; padding: 0 16px; }
1286
+ .approval-link { min-height: 44px; padding: 0 var(--so-space-lg); }
1273
1287
  .approval-edit[open] > summary { top: 6px; right: 6px; }
1274
1288
  .approval-field textarea, .approval-field input[type=text] { font-size: 16px; }
1275
1289
  .approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }
@@ -1719,7 +1733,7 @@
1719
1733
  .tabbar a { position: relative; }
1720
1734
  .tabbar a .dot-badge {
1721
1735
  position: absolute; top: .3125rem; left: calc(50% + .375rem);
1722
- width: .375rem; height: .375rem; border-radius: 9999px; background: var(--brand);
1736
+ width: .375rem; height: .375rem; border-radius: var(--so-radius-full); background: var(--brand);
1723
1737
  }
1724
1738
  .content > main { padding: 1rem 1rem calc(4.5rem + env(safe-area-inset-bottom, 0rem)); }
1725
1739
  .project-add-card { padding: 1rem; }
@@ -1752,7 +1766,7 @@
1752
1766
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1753
1767
  .stat-card .v { font-size: .75rem; color: var(--muted-foreground); margin-top: .25rem;
1754
1768
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1755
- .dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 9999px; flex: none; }
1769
+ .dot { display: inline-block; width: .5rem; height: .5rem; border-radius: var(--so-radius-full); flex: none; }
1756
1770
  .dot-ok { background: var(--success); }
1757
1771
  /* Caution without a claim on the operator: quiet, not magenta. */
1758
1772
  .dot-warn { background: var(--muted-foreground); }
@@ -1787,13 +1801,13 @@
1787
1801
  .sso-step-up { display: inline-flex; align-items: center; min-height: 36px; font-size: .9rem; }
1788
1802
  .sso-step-up[data-sso-step-up="confirmed"] { color: var(--so-success); }
1789
1803
  .sso-step-up[data-sso-step-up="confirmed"]::before { content: "✓"; margin-right: 6px; }
1790
- .login-shell .login-sso { display: flex; justify-content: center; align-items: center; min-height: 44px; border-radius: 8px; background: var(--so-accent); color: var(--so-on-accent); font-weight: 600; text-decoration: none; margin: 0 0 1rem; }
1804
+ .login-shell .login-sso { display: flex; justify-content: center; align-items: center; min-height: 44px; border-radius: var(--so-radius-md); background: var(--so-accent); color: var(--so-on-accent); font-weight: 600; text-decoration: none; margin: 0 0 1rem; }
1791
1805
  @media (hover: hover) and (pointer: fine) { .login-shell .login-sso:hover { background: var(--so-accent-hover); } }
1792
1806
  .login-shell .login-password > summary { text-align: center; font-size: .875rem; color: var(--muted-foreground); cursor: pointer; margin: 0 0 .75rem; }
1793
1807
  .focus-page { max-width: 40rem; margin: 0 auto; padding: 2rem 1.25rem 3rem; }
1794
1808
  .focus-page .focus-brand { display: inline-flex; margin: 0 0 2.5rem; }
1795
1809
  .focus-page h1 { margin: 0 0 1rem; }
1796
- .secret-value { overflow-wrap: anywhere; font-size: .95rem; padding: .75rem .875rem; margin: .75rem 0; border: 1px solid var(--border); border-radius: 8px; background: var(--background); user-select: all; }
1810
+ .secret-value { overflow-wrap: anywhere; font-size: .95rem; padding: .75rem .875rem; margin: .75rem 0; border: 1px solid var(--border); border-radius: var(--so-radius-md); background: var(--background); user-select: all; }
1797
1811
 
1798
1812
  @media (max-width: 640px) {
1799
1813
  button, form.option button { min-height: 2.75rem; }
@@ -1845,7 +1859,7 @@
1845
1859
  .lane .hint { margin-top: 0; }
1846
1860
  .lane-count { color: var(--muted-foreground); font-weight: 400; font-variant-numeric: tabular-nums; }
1847
1861
  .lane h2::before {
1848
- content: ""; width: .375rem; height: .375rem; border-radius: 9999px; flex: none;
1862
+ content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none;
1849
1863
  background: var(--muted-foreground); opacity: .55;
1850
1864
  }
1851
1865
  .lane-attention h2::before { background: var(--brand); opacity: 1; }
@@ -1901,7 +1915,7 @@
1901
1915
  .command-metric .value { display: block; margin-top: .25rem; font-size: 1.5rem; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
1902
1916
  .command-metric .label { display: flex; align-items: center; gap: .4rem; font-weight: 500; font-size: .8125rem; }
1903
1917
  .command-metric .label::before {
1904
- content: ""; width: .375rem; height: .375rem; border-radius: 9999px;
1918
+ content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full);
1905
1919
  background: var(--muted-foreground); flex: none;
1906
1920
  }
1907
1921
  .command-metric .detail { display: block; color: var(--muted-foreground); font-size: .6875rem; margin-top: .18rem; }
@@ -2433,7 +2447,7 @@
2433
2447
  }
2434
2448
  .workspace-stats .pulse-stat b { display: block; color: var(--foreground); font-weight: 600; font-family: var(--font-mono); font-size: 1.125rem; line-height: 1.2; font-variant-numeric: tabular-nums; }
2435
2449
  .workspace-stats .pulse-stat.hot b { color: var(--brand); }
2436
- .workspace-bar { display: flex; gap: 2px; height: .375rem; margin-top: .625rem; border-radius: 9999px; overflow: hidden; background: var(--muted); }
2450
+ .workspace-bar { display: flex; gap: 2px; height: .375rem; margin-top: .625rem; border-radius: var(--so-radius-full); overflow: hidden; background: var(--muted); }
2437
2451
  .workspace-bar .seg { flex: 1 1 0; }
2438
2452
  .workspace-bar .seg.attention { background: var(--brand); }
2439
2453
  .workspace-bar .seg.building { background: var(--running); }