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,4 +1,3 @@
1
- /** The conversation: chat and lead pages, proposal cards and the task chat. */
2
1
  import { assignmentSummaryHtml } from "../assignment-ui.js";
3
2
  import {} from "../assignment.js";
4
3
  import {} from "../browser-workspace.js";
@@ -27,7 +26,7 @@ import { assignmentStageOf, stageOfCode } from "../task-status.js";
27
26
  import {} from "../team-chat-authorization.js";
28
27
  import {} from "../team-contract.js";
29
28
  import { buildProgressOf } from "../workspace-ui.js";
30
- import { html, htmlString, isHtml, joinHtml, postForm, replaceMarkup } from "../html.js";
29
+ import { html, htmlString, isHtml, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
31
30
  import { requestContext, withFormToken } from "./request-context.js";
32
31
  import { createHash, randomBytes } from "node:crypto";
33
32
  import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon } from "./chrome.js";
@@ -90,7 +89,9 @@ export function taskChatApproval(focus, csrf) {
90
89
  repairChain: approval.repairChain,
91
90
  route: focus.route,
92
91
  coordinator: approval.coordinator,
92
+ origin: approval.origin ?? null,
93
93
  deliverable: approval.deliverable,
94
+ rebuild: focus.assignment?.need != null && "key" in focus.assignment.need && focus.assignment.need.key === "replan" ? focus.assignment.need.build : null,
94
95
  // Edit plan opens the task page's sheet with its fields open for editing.
95
96
  editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
96
97
  notNowHref: "/chat",
@@ -116,12 +117,15 @@ export function taskChatLiveRegion(focus, csrf, fragment = false, inert = false,
116
117
  : fragment && focus.approval !== null && focus.plan !== "requested"
117
118
  ? html `<section class="card chat-action-card chat-refresh-action"><a class="button-link" href="${approvalHref}" data-primary-action>Approve plan</a></section>`
118
119
  : taskChatApproval(focus, csrf));
119
- const approvalCard = focus.approval !== null && focus.dispatch?.action !== "approve-scope"
120
+ // The approval leads when dispatch waits on it or the plan was rewritten after a build (the assignment's one action).
121
+ const approvalLeads = focus.dispatch?.action === "approve-scope" ||
122
+ (focus.assignment?.primaryAction?.code === "approve-scope" && focus.assignment.primaryAction.target.taskId === focus.executionId);
123
+ const approvalCard = focus.approval !== null && !approvalLeads
120
124
  ? html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approvalContent}</details>` : approvalContent;
121
125
  const polling = !inert && ((focus.approval === null || focus.plan === "requested") && focus.state !== "done" && focus.state !== "cancelled" || focus.control.kind === "stopping" || focus.control.kind === "stop");
122
126
  const status = focus.assignment !== null
123
- ? html `${assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) })}${receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : ""}`
124
- : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope");
127
+ ? html `${assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && approvalLeads, sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) })}${receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : ""}`
128
+ : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && approvalLeads);
125
129
  const publication = focus.publication;
126
130
  const prUrl = publication === null ? null : safePrUrl(publication.prUrl);
127
131
  return html `<section id="task-chat-live" aria-live="polite" data-task="${focus.id}" data-execution="${focus.executionId}" data-source="/chat/task-status?task=${encodeURIComponent(focus.id)}" data-poll="${polling ? "1" : "0"}" data-approval="${focus.approval?.digest ?? ""}" data-plan="${focus.plan ?? ""}">${focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : ""}<div class="task-live-summary">${status}${checkProgressHtml(focus.checkProgress)}${
@@ -406,12 +410,12 @@ export function chatPage(chrome, data) {
406
410
  const candidates = data.chat === null ? [] : [...data.chat.candidates.values()];
407
411
  for (const one of candidates) {
408
412
  const draft = one.draft;
409
- parts.push(html `<div class="card"><p><strong>${draft.title}</strong> <span class="badge">Draft ${draft.kind}</span></p><p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p><p style="white-space:pre-wrap">${draft.goal}</p>${draft.outOfScope === null ? "" : html `<p class="meta">Not: ${draft.outOfScope}</p>`}${draft.touches.length > 0 ? html `<p class="meta">Touches: ${draft.touches.join(", ")}</p>` : ""}<p class="meta">Repo: <span class="mono">${projectName(one.repoPath)}</span></p>${postForm(`/chat/file/${one.key}`, html `<input type="password" name="token" placeholder="your password" autocomplete="current-password"><button type="submit">File unapproved</button>`, { attrs: { class: "inline" } })}</div>`);
413
+ parts.push(html `<div class="card"><p><strong>${draft.title}</strong> <span class="badge">Draft ${draft.kind}</span></p><p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p><p style="white-space:pre-wrap">${draft.goal}</p>${draft.outOfScope === null ? "" : html `<p class="meta">Not: ${draft.outOfScope}</p>`}${draft.touches.length > 0 ? html `<p class="meta">Touches: ${draft.touches.join(", ")}</p>` : ""}<p class="meta">Repo: <span class="mono">${projectName(one.repoPath)}</span></p>${postForm(`/chat/file/${one.key}`, html `${reauth(html `<input type="password" name="token" placeholder="your password" autocomplete="current-password">`)}<button type="submit">File unapproved</button>`, { attrs: { class: "inline" } })}</div>`);
410
414
  }
411
415
  if (!subscription && data.canManage && data.focusTask === null) {
412
416
  parts.push(html `<h2>Ask</h2>`, postForm("/chat", joinHtml([
413
417
  html `<label>Message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`,
414
- html `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`,
418
+ html `${reauth(html `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`)}`,
415
419
  html `<button type="submit">Ask</button>`,
416
420
  ], "\n"), { attrs: { class: "card" } }));
417
421
  }
@@ -423,15 +427,20 @@ export function chatPage(chrome, data) {
423
427
  }
424
428
  return screen("chat", chatWorkspace(joinHtml(parts, "\n"), data.projects, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
425
429
  }
426
- export function chatAckPage(chrome, turn, nonce) {
430
+ export function chatAckPage(chrome, turn, nonce, reply = null) {
431
+ // A lead reply's step reserves nothing itself: the reply set the money aside, and a reply whose cost is unknown has
432
+ // already counted that whole amount against its conversation. Say that amount, never the step's zero.
433
+ const replyCharge = reply === null ? null : reply.settledMicrousd ?? reply.reservedMicrousd;
427
434
  return screen("chat", joinHtml([
428
435
  html `<h1>Unknown spend</h1>`,
429
436
  html `<div class="card">`,
430
437
  html `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${turn.provider} · ${turn.model}</span> may have started before it failed (${turn.failureReason ?? "crashed"}), and its cost could not be measured.</p>`,
431
- html `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
438
+ replyCharge === null
439
+ ? html `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`
440
+ : html `<p><strong>This lead reply's worst case, ${chatMoney(replyCharge)}, has already been counted against its conversation. Acknowledging adds nothing more and re-enables chat on this credential.</strong></p>`,
432
441
  html `<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
433
442
  postForm(`/chat/ack/${turn.id}`, joinHtml([
434
- html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
443
+ html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`)}`,
435
444
  html `<button type="submit">Accept the charge — re-enable chat</button>`,
436
445
  ], "\n"), { hidden: { nonce } }),
437
446
  html `</div>`,
@@ -752,9 +761,26 @@ export function replacedThreadHtml(previous) {
752
761
  ? html `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${one.text}</p></div>`
753
762
  : html `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`)}<p class="chat-previous-divider" role="separator" data-thread-divider>${REPLACED_THREAD_DIVIDER}</p></section>`;
754
763
  }
764
+ /** A queued message's state under its words: waiting (with Remove), or why it was not sent. Nothing for one that ran.
765
+ * A div, not a p: the Remove form can't sit inside a paragraph (the parser would close it first). */
766
+ function queuedStateHtml(row, back) {
767
+ if (row.status === "queued") {
768
+ const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "It goes to the lead when it finishes.";
769
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="queued"><span class="so-queued-state">Queued</span> · ${waiting}${postForm(`/chat/queued/${row.message}/remove`, html `<button type="submit" class="so-queued-remove quiet">Remove</button>`, { attrs: { class: "inline" }, ...(back === null ? {} : { returnTo: back }) })}</div>`;
770
+ }
771
+ if (row.status === "running" && row.turnId === null)
772
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="starting"><span class="so-queued-state">Starting</span></div>`;
773
+ if (row.status === "cancelled" || row.status === "failed")
774
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="${row.status}"><span class="so-queued-state">Not sent</span> · ${row.error ?? "It was not sent."}</div>`;
775
+ if (row.status === "uncertain")
776
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="uncertain"><span class="so-queued-state">Not confirmed</span> · ${row.error ?? "Delivery could not be confirmed. Nothing was sent again."}</div>`;
777
+ return html ``;
778
+ }
755
779
  /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
756
780
  * "Something else" moves to the composer. Nothing shows while a reply is running. */
757
- export function leadAskHtml(ask, target, composer, open = true) {
781
+ export function leadAskHtml(ask, target, composer, open = true, stale = false) {
782
+ if (stale)
783
+ return html `<div class="so-owner-ask" data-ask="${ask.turn}" data-ask-stale><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><p class="so-owner-ask-stale">Out of date: the task changed after this was asked. Its status above says what it needs now.</p></div>`;
758
784
  if (!open)
759
785
  return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p></div>`;
760
786
  const hidden = target.task !== null ? { task: target.task } : target.project !== null ? { project: target.project } : {};
@@ -767,7 +793,7 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
767
793
  const open = rows.pending === null ? rows.ask?.turn ?? null : null;
768
794
  const asked = (turn) => {
769
795
  const ask = turn === null ? undefined : rows.asks?.get(turn);
770
- return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
796
+ return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open, ask.turn === rows.staleAsk);
771
797
  };
772
798
  // The owner's message each reply answers: when it asked for ids, the reply keeps them.
773
799
  const askedBy = new Map();
@@ -776,7 +802,9 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
776
802
  askedBy.set(message.id, last); return last; }, undefined);
777
803
  return rows.messages.map(message => ({
778
804
  id: message.id, role: message.role, text: message.text,
779
- html: htmlString(message.role === 'operator' ? html `<p>${message.text}</p>` : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
805
+ html: htmlString(message.role === 'operator'
806
+ ? (() => { const queued = rows.queue?.get(message.id); return html `${message.text === "" ? "" : html `<p>${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, back)}`; })()
807
+ : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
780
808
  activity: message.activity, createdAt: message.createdAt,
781
809
  ...(() => {
782
810
  // Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
@@ -796,6 +824,7 @@ export function leadChatVersion(rows) {
796
824
  const focus = rows.focusTask;
797
825
  const facts = [
798
826
  rows.messages.map(one => [one.id, one.role, one.turn]),
827
+ rows.queue === undefined ? null : [...rows.queue.values()].map(one => [one.message, one.status, one.revision, one.error]),
799
828
  rows.previous == null ? null : rows.previous.messages.map(one => one.id),
800
829
  rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
801
830
  rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
@@ -851,7 +880,9 @@ export function leadThreadHtml(data) {
851
880
  for (const message of data.messages) {
852
881
  if (message.role === "operator") {
853
882
  asked = message.text;
854
- thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}"><p style="white-space:pre-wrap">${message.text}</p></div>`);
883
+ // A message that waits its turn (the lead queue) says so, with Remove while it still waits — as in the React thread.
884
+ const queued = data.queue?.get(message.id);
885
+ thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}">${message.text === "" ? "" : html `<p style="white-space:pre-wrap">${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, returnTo)}</div>`);
855
886
  continue;
856
887
  }
857
888
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
@@ -917,7 +948,7 @@ export function leadPage(chrome, data) {
917
948
  // keyboard reaches it. Only this act moves the reader.
918
949
  html `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, postForm("/chat", joinHtml([
919
950
  html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${LEAD_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
920
- html `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : html ` disabled`}>Send</button>`,
951
+ html `<button type="submit" aria-label="${data.pending === null ? "send message" : "send message — it waits for the current reply"}">Send</button>`,
921
952
  ], "\n"), {
922
953
  attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session": data.session.id, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": data.session.approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": leadChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
923
954
  hidden: { request: randomBytes(16).toString("hex"), "request-session": data.session.id, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
@@ -925,7 +956,7 @@ export function leadPage(chrome, data) {
925
956
  // Concise pass (2026-09-13): the status line speaks only when there is
926
957
  // a state to report — a reply in progress here, the connection from
927
958
  // the continuity script — and the composer carries no second intro.
928
- html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. You can draft your next message or come back later."}</p>`,
959
+ html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. A message you send now waits its turn."}</p>`,
929
960
  // The explicit reconnection (package 2): shown only once the session
930
961
  // or sign-in changed under this page; it reloads on the reader's act.
931
962
  html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, leadAfterComposerHtml(data), controlsHtml);
@@ -1,4 +1,5 @@
1
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
+ import { type TaskOrigin } from "../task-source.js";
2
3
  import { type Html } from "../html.js";
3
4
  import { type AssignmentSnapshot } from "../assignment.js";
4
5
  import { type BoardCard } from "../board.js";
@@ -360,7 +361,7 @@ export declare function portfolioOverview(data: {
360
361
  startedAt: string;
361
362
  ranMinutes: number | null;
362
363
  costUsd: number | null;
363
- authMode: "subscription" | "api-key" | null;
364
+ authMode: "subscription" | "api-key" | "agent-login" | null;
364
365
  prNumber: number | null;
365
366
  prUrl: string | null;
366
367
  }[];
@@ -519,6 +520,8 @@ export declare function nextPage(chrome: Chrome, data: {
519
520
  deliverable?: "branch" | "report";
520
521
  /** v48: the agents the yes freezes — the one block every ceremony shows. */
521
522
  route: RouteView | null;
523
+ /** v126: where the work came from (an outside event's line of context, never a term agreed to). */
524
+ origin?: TaskOrigin | null;
522
525
  csrf: string;
523
526
  nonce: string;
524
527
  remaining: number;
@@ -1,5 +1,6 @@
1
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
- import { html, htmlString, joinHtml, postForm, replaceMarkup } from "../html.js";
2
+ import { originNote } from "../task-source.js";
3
+ import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
3
4
  import {} from "../assignment.js";
4
5
  import {} from "../board.js";
5
6
  import { browserWorkActionHref } from "../browser-workspace.js";
@@ -912,7 +913,7 @@ export function projectsPage(chrome, recent, candidates, open, problem, _unscope
912
913
  onboard.record[1].large
913
914
  ? html `<label class="row"><input type="checkbox" name="big-ok" value="1"> this is a large repository (or its size is unknown) — clone it anyway</label>`
914
915
  : null,
915
- html `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
916
+ html `${reauth(html `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`)}`,
916
917
  html `<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
917
918
  ], "\n"), { hidden: { nonce: onboard.record[0] } }),
918
919
  html `</div>`,
@@ -1258,11 +1259,11 @@ export function nextPage(chrome, data) {
1258
1259
  card =
1259
1260
  html `<h1>${item.approval.taskId}</h1><p>${item.approval.title}</p>${data.planDocument === null
1260
1261
  ? null
1261
- : html `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`}${postForm(`${taskHref(item.approval.taskId)}/approve`, html `<p><strong>Approve exactly this:</strong></p>${data.deliverable === "report" ? html `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : null}${scope === null || scope.profileState !== "unresolved" ? null : profileWords(scope)}<p class="meta">Goal</p><p class="recap" style="margin-top:0">${scope?.goal ?? item.approval.goal}</p><p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? html `<em>no exclusions</em>` : scope.outOfScope}</p><p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : joinHtml(scope.touches, ", ")}</p>${scope === null ? null : acceptanceCeremonyHtml(scope.acceptance)}${
1262
+ : html `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`}${postForm(`${taskHref(item.approval.taskId)}/approve`, html `<p><strong>Approve exactly this:</strong></p>${data.deliverable === "report" ? html `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : null}${scope === null || scope.profileState !== "unresolved" ? null : profileWords(scope)}${originNote(data.origin) === null ? null : html `<p class="meta" data-approval-origin>${originNote(data.origin)}</p>`}<p class="meta">Goal</p><p class="recap" style="margin-top:0">${scope?.goal ?? item.approval.goal}</p><p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? html `<em>no exclusions</em>` : scope.outOfScope}</p><p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : joinHtml(scope.touches, ", ")}</p>${scope === null ? null : acceptanceCeremonyHtml(scope.acceptance)}${
1262
1263
  // The AGENTS the yes freezes (v48): the same concise block the task
1263
1264
  // page and chat sign under, before the password — runtime limits one
1264
1265
  // tap away, never in the way.
1265
- agentsCeremonyHtml(data.route)}${scope === null ? null : runtimeDetailsHtml(scope)}<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label><div class="sticky-actions"><button type="submit">Approve this scope</button></div>`, { attrs: { class: "card approve-form" }, returnTo: "next", hidden: { nonce: data.nonce, digest: data.approvalDigest ?? item.approval.digest } })}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
1266
+ agentsCeremonyHtml(data.route)}${scope === null ? null : runtimeDetailsHtml(scope)}${reauth(html `<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>`)}<div class="sticky-actions"><button type="submit">Approve this scope</button></div>`, { attrs: { class: "card approve-form" }, returnTo: "next", hidden: { nonce: data.nonce, digest: data.approvalDigest ?? item.approval.digest } })}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
1266
1267
  }
1267
1268
  else if (item.kind === "requeue") {
1268
1269
  card =
@@ -7,8 +7,9 @@ export declare function joinDeadPage(): Html;
7
7
  /**
8
8
  * v100: for someone signed in with the identity provider, every step-up
9
9
  * password field (autocomplete="current-password"; a secret like a bot token
10
- * is never one) becomes that sign-in: confirmed, when the provider checked
11
- * them in the last ten minutes, or a link to be checked again, then back here.
10
+ * is never one) becomes that sign-in: confirmed, when an explicit re-check
11
+ * made on request awaits its one step-up (cockpit B2), or a link to be
12
+ * checked again, then back here. An ordinary sign-in never confirms.
12
13
  */
13
14
  export declare function ssoStepUps(page: Html, sso: {
14
15
  label: string;
@@ -33,8 +33,9 @@ export function joinDeadPage() {
33
33
  /**
34
34
  * v100: for someone signed in with the identity provider, every step-up
35
35
  * password field (autocomplete="current-password"; a secret like a bot token
36
- * is never one) becomes that sign-in: confirmed, when the provider checked
37
- * them in the last ten minutes, or a link to be checked again, then back here.
36
+ * is never one) becomes that sign-in: confirmed, when an explicit re-check
37
+ * made on request awaits its one step-up (cockpit B2), or a link to be
38
+ * checked again, then back here. An ordinary sign-in never confirms.
38
39
  */
39
40
  export function ssoStepUps(page, sso, returnTo) {
40
41
  const stepUp = (attributes) => /\btype="password"/.test(attributes) && /\bautocomplete="current-password"/.test(attributes);
@@ -1,3 +1,5 @@
1
+ /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
+ import type { RunNetworkRecord } from "../egress.js";
1
3
  import { type AssignmentSnapshot } from "../assignment.js";
2
4
  import { type BrowserCheckItem, type BrowserNeedAction, type BrowserResultPanel, type BrowserResultView } from "../browser-workspace.js";
3
5
  import { type ReportShot } from "../evidence.js";
@@ -7,6 +9,7 @@ import { type CriterionMatrixRow, type ProofVerdict } from "../proof.js";
7
9
  import { type PullRequestView } from "../pull-request-flow.js";
8
10
  import { type StructuredHandoffView, type TerminalDiffView } from "../result-evidence-readers.js";
9
11
  import { type FollowUpCheck } from "../result-follow-ups.js";
12
+ import type { RunEfficiency } from "../run-efficiency.js";
10
13
  import { type ResultScreenshot, type ResultTab, type SharedResultFacts } from "../result-review.js";
11
14
  import { type BuildReviewView } from "../review-switch.js";
12
15
  import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
@@ -180,6 +183,10 @@ export type RankedReviewRow = CompletedWorkRow & {
180
183
  * scope, plan, run, artifact, verdict, and publication records,
181
184
  * each read through the verifier the run page already uses. */
182
185
  export type ReviewCockpitView = {
186
+ /** How the result's run went (docs/plans/run-efficiency.md): display only, its sources and their gaps said. */
187
+ efficiency?: RunEfficiency | null;
188
+ /** What the run's network was and what its proxy saw (egress.ts): display only. */
189
+ network?: RunNetworkRecord | null;
183
190
  taskId: string;
184
191
  title: string;
185
192
  repo: string | null;
@@ -285,7 +292,16 @@ export declare function reviewCockpitPage(chrome: Chrome, data: {
285
292
  export declare function reviewCockpitDetail(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview?: boolean, tab?: ResultTab, user?: string): Html;
286
293
  /** The selected result's HTML and, for the rebuilt page, the same result
287
294
  * as data: every form and panel body is the one the HTML carries. */
288
- export declare function reviewCockpitDetailParts(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview: boolean, tab: ResultTab, user: string): {
295
+ /** Where a result is shown: its own page (/review), or the Result tab of its task's page (docs/plans/one-page-task-and-agent.md),
296
+ * whose address is `base` (the task page with tab=result, and its version when not the current one). The result's own
297
+ * view (Summary / Changes / Checks) rides `section=` there, so it never fights the task page's `tab`. */
298
+ export type ResultHost = {
299
+ kind: "review";
300
+ } | {
301
+ kind: "task";
302
+ base: string;
303
+ };
304
+ export declare function reviewCockpitDetailParts(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview: boolean, tab: ResultTab, user: string, host?: ResultHost): {
289
305
  html: Html;
290
306
  selected: NonNullable<BrowserResultView["selected"]>;
291
307
  };
@@ -319,7 +335,7 @@ export declare function pullRequestCardHtml(pr: TaskPullRequest, csrf: string):
319
335
  * bearer session (no token) sees the road named, never a form. */
320
336
  export declare function reviewNextAction(view: ReviewCockpitView, csrf: string, _accepted: boolean, _canAnnotate: boolean): Html;
321
337
  /** The next action for the browser workspace's JSON: its control as markup text. */
322
- export declare function reviewNextActionOf(view: ReviewCockpitView, csrf: string): {
338
+ export declare function reviewNextActionOf(view: ReviewCockpitView, csrf: string, back?: string): {
323
339
  kind: string;
324
340
  title: string;
325
341
  detail: string;
@@ -1,4 +1,3 @@
1
- /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
1
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from "../assignment-presentation.js";
3
2
  import { assignmentActionHref, assignmentStatusOf, assignmentWithEvidence } from "../assignment-ui.js";
4
3
  import { owedAcceptance } from "../assignment.js";
@@ -26,7 +25,7 @@ import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf,
26
25
  import { createHash, randomBytes } from "node:crypto";
27
26
  import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
28
27
  import { chatResultHref, reviewHref, taskChatHref, taskHref } from "./http.js";
29
- import { attributes, html, htmlString, joinHtml, postForm } from "../html.js";
28
+ import { attributes, html, htmlString, joinHtml, postForm, reauth } from "../html.js";
30
29
  import { publicationFactsOf, statusActionHref, statusLineHtml } from "./render-tasks.js";
31
30
  export const RUNS_PAGE = 50;
32
31
  /**
@@ -574,23 +573,23 @@ export function reviewCockpitPage(chrome, data) {
574
573
  export function reviewCockpitDetail(view, csrf, noted, canRetryReview = false, tab = "summary", user = "") {
575
574
  return reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user).html;
576
575
  }
577
- /** The selected result's HTML and, for the rebuilt page, the same result
578
- * as data: every form and panel body is the one the HTML carries. */
579
- export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user) {
576
+ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user, host = { kind: "review" }) {
580
577
  const parts = [];
581
578
  const run = view.run;
582
579
  const proof = view.detail?.proof ?? null;
583
580
  const accepted = proof?.accepted !== null && proof?.accepted !== undefined;
584
581
  const canAnnotate = canAnnotateDiff(view, csrf);
585
582
  const status = cockpitStatusOf(view);
586
- // Header: what this is, its verdict word, and why it sits where it does.
587
- parts.push(joinHtml([
588
- html `<header class="cockpit-head" data-review-task="${view.taskId}">`,
589
- html `<h1>${view.title}</h1>`,
590
- html `<p class="meta">${run === null ? "No build" : `Build #${run.id}`} · <a href="${taskHref(view.taskId)}">Open task</a>${projectChip(view.repo)}</p>`,
591
- html `<p class="cockpit-chips">${statusLineHtml(status)}</p>`,
592
- html `</header>`,
593
- ]));
583
+ // Header: what this is, its verdict word, and why it sits where it does. On a task page the task's own header says
584
+ // all of it, once.
585
+ if (host.kind === "review")
586
+ parts.push(joinHtml([
587
+ html `<header class="cockpit-head" data-review-task="${view.taskId}">`,
588
+ html `<h1>${view.title}</h1>`,
589
+ html `<p class="meta">${run === null ? "No build" : `Build #${run.id}`} · <a href="${taskHref(view.taskId)}">Open task</a>${projectChip(view.repo)}</p>`,
590
+ html `<p class="cockpit-chips">${statusLineHtml(status)}</p>`,
591
+ html `</header>`,
592
+ ]));
594
593
  if (view.detail === null && view.historyProblem !== null)
595
594
  parts.push(html `<p class="problem" data-history-problem>${view.historyProblem} <a href="${taskHref(view.taskId)}">Open task</a></p>`);
596
595
  // The primary next act — exactly one road, chosen from the state.
@@ -629,8 +628,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
629
628
  taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
630
629
  status: { label: status.label, tone: status.tone, token: status.token },
631
630
  problem: view.detail === null ? view.historyProblem : null,
632
- next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
631
+ next: reviewNextActionOf(view, csrf, host.kind === "task" && run !== null ? `${host.base}&run=${run.id}` : undefined), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
633
632
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
633
+ efficiency: view.efficiency ?? null,
634
+ network: view.network ?? null,
634
635
  acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
635
636
  // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
636
637
  record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
@@ -651,6 +652,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
651
652
  // v50 review-retry panel and the accept-with-exception form.
652
653
  const extraChecks = reviewRetryPanel(view.taskId, run.id, view.reviewRetry, { csrf, canAct: false, returnTo: null });
653
654
  const here = reviewHref(view.taskId);
655
+ /** This result's own address on its host, at one of its views (summary when none). */
656
+ const at = (section = "summary") => host.kind === "task"
657
+ ? `${host.base}&run=${run.id}${section === "summary" ? "" : `&section=${section}`}`
658
+ : `${here}&run=${run.id}${section === "summary" ? "" : `&tab=${section}`}`;
654
659
  const assignment = view.assignment === null ? null : assignmentWithEvidence(view.assignment, receiptStatusOf(view.detail.receipt), run.id);
655
660
  const checks = assignment?.receipt?.checks;
656
661
  if (checks !== undefined)
@@ -664,13 +669,16 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
664
669
  user,
665
670
  noted,
666
671
  requestToken: randomBytes(16).toString("hex"),
667
- hrefFor: one => `${here}&run=${run.id}${one === "summary" ? "" : `&tab=${one}`}`,
668
- returnTo: `${here}&run=${run.id}`,
672
+ hrefFor: one => at(one),
673
+ returnTo: at(),
669
674
  back: null,
670
675
  extraChecks,
671
676
  headStatus: false,
672
677
  action: false,
673
678
  });
679
+ // On a task page the result script keeps the result's own view in `section=` (the page's `tab` is the task's).
680
+ if (host.kind === "task")
681
+ panel.panel.attributes = { ...panel.panel.attributes, "data-result-param": "section" };
674
682
  parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
675
683
  // What no acceptance resolves (a failed check, a HIGH finding): Complete is refused, so no Accept is offered and
676
684
  // the one line says the next step.
@@ -682,7 +690,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
682
690
  const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
683
691
  : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
684
692
  const complete = hardStop === null && assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
685
- ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
693
+ ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed, ...(host.kind === "task" ? { returnTo: at() } : {}) } : null;
686
694
  if (complete !== null)
687
695
  parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
688
696
  // The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
@@ -708,13 +716,14 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
708
716
  const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
709
717
  (checks === undefined || ((checks.status === "not-run" || checks.status === "unavailable") && checks.release == null))
710
718
  // Back on this result's Checks tab, where #follow-ups shows the run it started.
711
- ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
719
+ ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: at("checks") } : null;
712
720
  const need = panel.panel.need;
713
721
  const nextKind = selected.next?.kind;
714
722
  // A failed build: what went wrong is the card, Retry the act; a link to this same page goes nowhere, so it isn't one.
715
723
  const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
716
- link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
717
- ...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
724
+ link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null
725
+ : host.kind === "task" && view.failure.link.href.startsWith(`${here}&run=${run.id}&tab=`) ? { ...view.failure.link, href: view.failure.link.href.replace(`${here}&run=${run.id}&tab=`, `${host.base}&run=${run.id}&section=`) } : view.failure.link,
726
+ ...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: at() } }) };
718
727
  const actFacts = failure !== null
719
728
  ? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
720
729
  : {
@@ -744,7 +753,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
744
753
  const lines = (added.length > 0 ? added : shown).map(line => line.newLine);
745
754
  return [file.path, lines.length === 0 ? null : { from: Math.min(...lines), to: Math.max(...lines) }];
746
755
  }));
747
- const changesHref = `${here}&run=${run.id}&tab=changes`;
756
+ const changesHref = at("changes");
748
757
  const found = reportMismatchesOf(proof.reasons, proof.proofProblem === null ? proof.matrix : [], changes);
749
758
  // The saved report itself can't be read: that is the evidence problem, said once.
750
759
  if (proof.proofProblem !== null)
@@ -757,7 +766,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
757
766
  text: one.text, path: one.path, absent: one.inChanges === false,
758
767
  lines: one.lines === null ? null : one.lines.from === one.lines.to ? `line ${one.lines.from}` : `lines ${one.lines.from}–${one.lines.to}`,
759
768
  href: one.path !== null && one.inChanges === true ? `${changesHref}#${diffFileAnchor(one.path)}${hunkStart.has(one.path) ? `-L${hunkStart.get(one.path)}` : ""}`
760
- : one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${here}&run=${run.id}#report-note-${one.note}` : null,
769
+ : one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${at()}#report-note-${one.note}` : null,
761
770
  noteLabel: one.path === null && one.note !== null ? `The report, note ${one.note}` : null,
762
771
  })),
763
772
  said: [...new Set([...proof.reasons, ...found.map(one => one.reason)].flatMap(reason => [reason, plainReasonWords(reason)]))],
@@ -825,7 +834,7 @@ export function pullRequestCardHtml(pr, csrf) {
825
834
  const merged = view.mergeCommit === null ? "" : html `<p class="meta">Merge commit <span class="mono">${view.mergeCommit.slice(0, 12)}</span></p>`;
826
835
  const target = pr.target === null ? "the base branch" : pr.target;
827
836
  const act = !view.canMerge || csrf === "" || view.prNumber === null ? "" :
828
- postForm(`${taskHref(pr.taskId)}/merge`, html `<p class="meta">${view.mergeMethod === "squash" ? "Squash-merges" : view.mergeMethod === "rebase" ? "Rebase-merges" : "Merges"} PR #${view.prNumber} into ${target} and deletes its branch.</p><label>Password<input type="password" name="token" autocomplete="current-password" required></label>${view.fullCheck === null ? html `<button type="submit">Merge</button>` : html `<input type="hidden" name="anyway" value="1"><button type="submit" class="secondary">Merge anyway</button>`}`, { attrs: { class: "pull-request-act", "data-merge-form": true }, hidden: { run: view.runId } });
837
+ postForm(`${taskHref(pr.taskId)}/merge`, html `<p class="meta">${view.mergeMethod === "squash" ? "Squash-merges" : view.mergeMethod === "rebase" ? "Rebase-merges" : "Merges"} PR #${view.prNumber} into ${target} and deletes its branch.</p>${reauth(html `<label>Password<input type="password" name="token" autocomplete="current-password" required></label>`)}${view.fullCheck === null ? html `<button type="submit">Merge</button>` : html `<input type="hidden" name="anyway" value="1"><button type="submit" class="secondary">Merge anyway</button>`}`, { attrs: { class: "pull-request-act", "data-merge-form": true }, hidden: { run: view.runId } });
829
838
  return html `<section class="card pull-request pull-request--${tone}" id="merge" data-pull-request="${view.state}"><div class="pull-request-head"><strong>${view.label}</strong>${link}</div><p class="meta">${view.detail}${revision}</p>${merged}${act}</section>`;
830
839
  }
831
840
  /** Exactly one primary road per result, chosen from its state; the
@@ -837,11 +846,11 @@ export function reviewNextAction(view, csrf, _accepted, _canAnnotate) {
837
846
  return next === null ? html `` : html `<div class="card cockpit-next" data-next-action="${next.kind}"><div><strong>${next.title}</strong><span class="meta">${next.detail}</span></div>${next.control}</div>`;
838
847
  }
839
848
  /** The next action for the browser workspace's JSON: its control as markup text. */
840
- export function reviewNextActionOf(view, csrf) {
841
- const next = reviewNextActionParts(view, csrf);
849
+ export function reviewNextActionOf(view, csrf, back) {
850
+ const next = reviewNextActionParts(view, csrf, back);
842
851
  return next === null ? null : { ...next, control: htmlString(next.control) };
843
852
  }
844
- function reviewNextActionParts(view, csrf) {
853
+ function reviewNextActionParts(view, csrf, back) {
845
854
  const run = view.run;
846
855
  const detail = view.detail;
847
856
  const card = (kind, title, detailWords, control) => ({ kind, title, detail: detailWords, control });
@@ -852,7 +861,7 @@ function reviewNextActionParts(view, csrf) {
852
861
  return card("draft-repair", "CI is failing on its pull request", "Toolroll confirmed the failure. Draft one repair task, then approve it before it runs.", postForm(`/r/${run.id}/draft-repair`, html `<button type="submit">Draft a repair task</button>`));
853
862
  }
854
863
  if (detail.comments.length > 0 && csrf !== "") {
855
- return card("revise", `${detail.comments.length} note${detail.comments.length === 1 ? "" : "s"} ready`, "Create one revision from these notes. You approve it before it runs.", postForm(`/r/${run.id}/revise`, html `${revisionSealFields(detail.comments, detail.sourceDigest)}<button type="submit">Revise</button>`, { returnTo: reviewHref(view.taskId) }));
864
+ return card("revise", `${detail.comments.length} note${detail.comments.length === 1 ? "" : "s"} ready`, "Create one revision from these notes. You approve it before it runs.", postForm(`/r/${run.id}/revise`, html `${revisionSealFields(detail.comments, detail.sourceDigest)}<button type="submit">Revise</button>`, { returnTo: back ?? reviewHref(view.taskId) }));
856
865
  }
857
866
  return null;
858
867
  }
@@ -1982,7 +1991,7 @@ export function resultNeedAction(need, o, canRequest) {
1982
1991
  : need.rebuild != null
1983
1992
  ? postForm(need.rebuild.action, html `<button type="submit" data-primary-action data-rebuild style="min-height:44px">${need.label}</button>`, { attrs: { class: "rebuild" } })
1984
1993
  : need.confirm !== null
1985
- ? postForm(need.confirm.action, html `${need.confirm.checked === true ? html `<label><input type="checkbox" name="checked" value="yes" required> Nothing from build #${need.confirm.run} is running</label>` : ""}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><button type="submit" style="min-height:44px">${need.label}</button>`, { attrs: { id: "confirm-stopped", class: "confirm-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
1994
+ ? postForm(need.confirm.action, html `${need.confirm.checked === true ? html `<label><input type="checkbox" name="checked" value="yes" required> Nothing from build #${need.confirm.run} is running</label>` : ""}${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button type="submit" style="min-height:44px">${need.label}</button>`, { attrs: { id: "confirm-stopped", class: "confirm-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
1986
1995
  : html `<a class="button-link" href="${need.href ?? "#"}" data-primary-action>${need.label}</a>`;
1987
1996
  return html `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? html `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
1988
1997
  }
@@ -23,7 +23,7 @@ import { redactToken, TOKEN_ENV } from "../telegram.js";
23
23
  import { requestContext } from "./request-context.js";
24
24
  import { chatMoney, screen, strokeIcon, when, whenTime } from "./chrome.js";
25
25
  import { taskHref } from "./http.js";
26
- import { html, joinHtml, postForm } from "../html.js";
26
+ import { html, joinHtml, postForm, reauth } from "../html.js";
27
27
  import { permissionModeChoices, qualityModeChoices } from "./render-tasks.js";
28
28
  /** Settings → Lead: one line for what runs the lead and one action; the full form, turning it off and stored keys under Advanced. */
29
29
  export function leadSettingsHtml(data) {
@@ -36,7 +36,7 @@ export function leadSettingsHtml(data) {
36
36
  : data.words !== null
37
37
  ? html `<p>The lead uses ${data.words}.</p><p class="meta">${config.model === "default" ? "Its default model" : `Model ${config.model}`} · up to ${config.dailyTurns} turns a day · no dollar spend. Every action it proposes waits for you to confirm it.</p>`
38
38
  : html `<p>The lead uses the ${config.provider === "anthropic-api" ? "Anthropic" : "OpenRouter"} API with ${config.model}.</p><p class="meta">Up to ${chatMoney(config.weeklyCeilingMicrousd)} a week · up to ${config.dailyTurns} turns a day.</p>`;
39
- const forget = facts.keyFacts.filter(one => one.state === "stored").map(one => postForm("/chat/config", html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password"><button type="submit" class="secondary">Forget the stored ${one.provider} key</button>`, { attrs: { class: "inline" }, returnTo, hidden: { "forget-key": one.provider } }));
39
+ const forget = facts.keyFacts.filter(one => one.state === "stored").map(one => postForm("/chat/config", html `${reauth(html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password">`)}<button type="submit" class="secondary">Forget the stored ${one.provider} key</button>`, { attrs: { class: "inline" }, returnTo, hidden: { "forget-key": one.provider } }));
40
40
  return joinHtml([
41
41
  html `<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
42
42
  data.said === null ? "" : html `<p class="problem" role="status">${data.said}</p>`,
@@ -58,7 +58,7 @@ ${postForm("/settings/lead/promise/cancel", html `<button type="submit" class="s
58
58
  : html `<ul class="lead-subagent-list">${data.subagents.map(one => html `<li data-subagent="${one.id}"><a href="/settings/lead/subagents/${one.id}">${one.label}</a><span class="meta">${one.project}${one.paused ? " · Paused" : ""}</span></li>`)}</ul>`}<p><a class="button-link" href="/settings/lead/subagents?add=1#add" data-add-subagent>Add subagent</a></p></section>`,
59
59
  html `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
60
60
  leadConfigForm(config, facts),
61
- config === null ? "" : postForm("/chat/config", html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password"><button type="submit" class="secondary">Turn the lead off</button>`, { attrs: { class: "inline" }, returnTo, hidden: { off: "1" } }),
61
+ config === null ? "" : postForm("/chat/config", html `${reauth(html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password">`)}<button type="submit" class="secondary">Turn the lead off</button>`, { attrs: { class: "inline" }, returnTo, hidden: { off: "1" } }),
62
62
  forget,
63
63
  html `</details>`,
64
64
  ], "\n");
@@ -102,7 +102,7 @@ ${models.map(model => html `<option value="${model}">${labels.get(model) ?? ""}<
102
102
  .join(" · ")})</span>\
103
103
  <input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
104
104
  html `<label>Your password <span class="meta">(typed again to change the provider)</span>\
105
- <input type="password" name="token" autocomplete="current-password"></label>`,
105
+ ${reauth(html `<input type="password" name="token" autocomplete="current-password">`)}</label>`,
106
106
  html `<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
107
107
  "",
108
108
  ], "\n"), { attrs: { class: "card" }, returnTo: data.returnTo }),
@@ -290,7 +290,7 @@ ${[
290
290
  postForm("/push/subscribe", joinHtml([
291
291
  "",
292
292
  html `<input type="hidden" name="endpoint" value=""><input type="hidden" name="p256dh" value=""><input type="hidden" name="auth" value="">`,
293
- html `<label>Your password <input type="password" name="token" autocomplete="current-password"></label>`,
293
+ html `${reauth(html `<label>Your password <input type="password" name="token" autocomplete="current-password"></label>`)}`,
294
294
  html `<button type="submit" id="push-enable">Get alerts on this device</button>`,
295
295
  html `<p class="meta" id="push-state"></p>`,
296
296
  "",
@@ -528,6 +528,7 @@ export const SETTINGS_GROUPS = [
528
528
  ]],
529
529
  ["Access and rules", [
530
530
  ["/settings/sign-in", "Sign-in", html `<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>`],
531
+ ["/settings/passwordless", "Passwordless", html `<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>`],
531
532
  ["/settings/sessions", "Sessions & tokens", html `<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3"/>`],
532
533
  ["/settings/project", "Project", html `<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>`],
533
534
  ["/settings/policy", "Policy", html `<path d="M9 12l2 2 4-4"/><rect x="4" y="3" width="16" height="18" rx="2"/>`],