toolroll 0.9.63 → 0.9.67

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 (302) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +33 -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 +117 -2
  16. package/dist/agent-fence.js +439 -12
  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 +39 -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/batch-checks.js +3 -2
  32. package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
  33. package/dist/browser/terminal.css +37 -0
  34. package/dist/browser/terminal.js +228 -0
  35. package/dist/browser/workspace.css +2 -2
  36. package/dist/browser/workspace.js +42 -38
  37. package/dist/browser-shell.js +6 -3
  38. package/dist/browser-workspace.d.ts +235 -1
  39. package/dist/browser-workspace.js +60 -0
  40. package/dist/builder.d.ts +10 -1
  41. package/dist/builder.js +87 -24
  42. package/dist/chat-actions.d.ts +13 -1
  43. package/dist/chat-actions.js +197 -15
  44. package/dist/chat-ask.d.ts +4 -0
  45. package/dist/chat-ask.js +14 -0
  46. package/dist/chat-channel.js +20 -0
  47. package/dist/chat-continuity.js +5 -4
  48. package/dist/chat-decide.d.ts +0 -14
  49. package/dist/chat-decide.js +4 -1
  50. package/dist/chat-delivery.d.ts +9 -0
  51. package/dist/chat-delivery.js +45 -6
  52. package/dist/chat-messages.d.ts +8 -0
  53. package/dist/chat-messages.js +36 -4
  54. package/dist/check-levels-ui.js +2 -2
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +19 -0
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +4 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +21 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/console-api.d.ts +13 -4
  71. package/dist/contracts/console-api.js +13 -4
  72. package/dist/contracts/flow-card.d.ts +4 -0
  73. package/dist/contracts/flow-card.js +3 -0
  74. package/dist/contracts/flow.d.ts +46 -0
  75. package/dist/contracts/flow.js +11 -2
  76. package/dist/contracts/lead-tools.d.ts +92 -0
  77. package/dist/contracts/lead-tools.js +13 -0
  78. package/dist/contracts/provider.d.ts +11 -1
  79. package/dist/contracts/provider.js +1 -1
  80. package/dist/contracts/route.d.ts +76 -1
  81. package/dist/contracts/route.js +1 -1
  82. package/dist/contracts/store-json.d.ts +2 -0
  83. package/dist/control-setup.d.ts +1 -1
  84. package/dist/control-ui.js +2 -2
  85. package/dist/converse.d.ts +16 -0
  86. package/dist/converse.js +32 -4
  87. package/dist/demo-chat.js +4 -4
  88. package/dist/demo.js +2 -2
  89. package/dist/desktop-update-gate.js +6 -0
  90. package/dist/desktop-update.d.ts +1 -1
  91. package/dist/discord-chat.js +4 -1
  92. package/dist/doctor.js +1 -1
  93. package/dist/egress-bridge.d.ts +17 -0
  94. package/dist/egress-bridge.js +111 -0
  95. package/dist/egress.d.ts +119 -0
  96. package/dist/egress.js +423 -0
  97. package/dist/exec.d.ts +78 -0
  98. package/dist/exec.js +367 -9
  99. package/dist/export-ui.js +2 -2
  100. package/dist/flow-engine.js +24 -5
  101. package/dist/flow-gallery-ui.js +2 -2
  102. package/dist/flow-gallery.js +27 -8
  103. package/dist/flow-share.js +16 -5
  104. package/dist/flow-triggers.d.ts +77 -8
  105. package/dist/flow-triggers.js +517 -70
  106. package/dist/flows-cli.js +8 -2
  107. package/dist/flows-ui.d.ts +0 -2
  108. package/dist/flows-ui.js +52 -4
  109. package/dist/flows.d.ts +4 -0
  110. package/dist/flows.js +9 -1
  111. package/dist/html.d.ts +9 -0
  112. package/dist/html.js +11 -0
  113. package/dist/invoke.d.ts +35 -0
  114. package/dist/invoke.js +347 -28
  115. package/dist/keys.d.ts +18 -14
  116. package/dist/keys.js +5 -0
  117. package/dist/lead-capabilities.js +3 -2
  118. package/dist/lead-contract.js +2 -0
  119. package/dist/lead-doors.d.ts +2 -0
  120. package/dist/lead-doors.js +7 -0
  121. package/dist/lead-follow.d.ts +2 -0
  122. package/dist/lead-follow.js +58 -2
  123. package/dist/lead-progress.js +1 -0
  124. package/dist/lead-queue-runtime.d.ts +46 -0
  125. package/dist/lead-queue-runtime.js +191 -0
  126. package/dist/lead-queue.d.ts +104 -0
  127. package/dist/lead-queue.js +187 -0
  128. package/dist/lead-sweep.d.ts +67 -0
  129. package/dist/lead-sweep.js +99 -0
  130. package/dist/lead-tools.d.ts +22 -0
  131. package/dist/lead-tools.js +152 -1
  132. package/dist/lead.d.ts +13 -5
  133. package/dist/lead.js +89 -29
  134. package/dist/limits-ui.d.ts +8 -1
  135. package/dist/limits-ui.js +26 -0
  136. package/dist/live-bus.d.ts +8 -0
  137. package/dist/live-bus.js +40 -4
  138. package/dist/live-detail.d.ts +27 -0
  139. package/dist/live-detail.js +57 -0
  140. package/dist/live-notes.d.ts +20 -0
  141. package/dist/live-notes.js +47 -0
  142. package/dist/live-records.d.ts +140 -0
  143. package/dist/live-records.js +935 -0
  144. package/dist/live-terminal.d.ts +269 -0
  145. package/dist/live-terminal.js +686 -0
  146. package/dist/live.d.ts +38 -27
  147. package/dist/live.js +142 -124
  148. package/dist/mcp-oauth.d.ts +6 -3
  149. package/dist/mcp-oauth.js +8 -4
  150. package/dist/memory-pass.js +2 -2
  151. package/dist/mobile-viewport.d.ts +5 -1
  152. package/dist/mobile-viewport.js +18 -2
  153. package/dist/model-catalog.js +4 -0
  154. package/dist/monitoring-ui.js +2 -2
  155. package/dist/needs-you.d.ts +1 -1
  156. package/dist/needs-you.js +11 -4
  157. package/dist/operate.d.ts +4 -0
  158. package/dist/operate.js +335 -19
  159. package/dist/passwordless.d.ts +23 -0
  160. package/dist/passwordless.js +26 -0
  161. package/dist/peek-cli.d.ts +12 -4
  162. package/dist/peek-cli.js +19 -11
  163. package/dist/phase-routing.js +10 -3
  164. package/dist/planner-source.d.ts +3 -0
  165. package/dist/planner-source.js +14 -0
  166. package/dist/planner.d.ts +2 -1
  167. package/dist/planner.js +7 -1
  168. package/dist/policy-ui.js +2 -2
  169. package/dist/policy.d.ts +1 -20
  170. package/dist/policy.js +29 -2
  171. package/dist/project-delete-ui.d.ts +18 -0
  172. package/dist/project-delete-ui.js +60 -9
  173. package/dist/project-delete.js +2 -2
  174. package/dist/project-memory.d.ts +3 -1
  175. package/dist/project-memory.js +57 -25
  176. package/dist/project-tools.d.ts +5 -3
  177. package/dist/project-tools.js +12 -9
  178. package/dist/provider-auth.d.ts +4 -3
  179. package/dist/provider-auth.js +5 -4
  180. package/dist/provider-connection.d.ts +2 -1
  181. package/dist/provider-connection.js +5 -0
  182. package/dist/provider.d.ts +25 -2
  183. package/dist/provider.js +63 -9
  184. package/dist/pull-request-ui.js +2 -2
  185. package/dist/push.js +1 -1
  186. package/dist/quick-start.d.ts +24 -0
  187. package/dist/quick-start.js +127 -0
  188. package/dist/release-adapters.d.ts +6 -0
  189. package/dist/release-adapters.js +70 -3
  190. package/dist/release.d.ts +10 -1
  191. package/dist/release.js +34 -15
  192. package/dist/request-budget-ui.js +2 -2
  193. package/dist/result-follow-ups.js +3 -2
  194. package/dist/result-review.d.ts +1 -1
  195. package/dist/result-review.js +9 -3
  196. package/dist/retention-ui.js +2 -2
  197. package/dist/retention.js +10 -1
  198. package/dist/run-efficiency.d.ts +73 -0
  199. package/dist/run-efficiency.js +228 -0
  200. package/dist/runner.d.ts +5 -1
  201. package/dist/runner.js +22 -1
  202. package/dist/scope.d.ts +183 -5
  203. package/dist/scope.js +256 -31
  204. package/dist/scout-net.d.ts +4 -1
  205. package/dist/scout-net.js +37 -12
  206. package/dist/scout.d.ts +20 -6
  207. package/dist/scout.js +27 -3
  208. package/dist/serve.js +248 -40
  209. package/dist/server/chat.d.ts +1 -1
  210. package/dist/server/chat.js +122 -31
  211. package/dist/server/chrome.d.ts +16 -2
  212. package/dist/server/chrome.js +63 -11
  213. package/dist/server/console.css +107 -42
  214. package/dist/server/flows.js +38 -8
  215. package/dist/server/guards.d.ts +1 -0
  216. package/dist/server/guards.js +23 -9
  217. package/dist/server/http.d.ts +7 -0
  218. package/dist/server/http.js +3 -2
  219. package/dist/server/live.d.ts +34 -1
  220. package/dist/server/live.js +39 -17
  221. package/dist/server/pages-assets.js +5 -0
  222. package/dist/server/pages.js +90 -13
  223. package/dist/server/people-tokens.d.ts +3 -0
  224. package/dist/server/people-tokens.js +55 -17
  225. package/dist/server/remote-hooks.js +12 -6
  226. package/dist/server/remote.d.ts +2 -1
  227. package/dist/server/remote.js +14 -5
  228. package/dist/server/render-chat.d.ts +12 -3
  229. package/dist/server/render-chat.js +47 -16
  230. package/dist/server/render-pages.d.ts +32 -4
  231. package/dist/server/render-pages.js +83 -13
  232. package/dist/server/render-people.d.ts +3 -2
  233. package/dist/server/render-people.js +3 -2
  234. package/dist/server/render-results.d.ts +18 -2
  235. package/dist/server/render-results.js +37 -28
  236. package/dist/server/render-settings.js +6 -5
  237. package/dist/server/render-tasks.d.ts +35 -10
  238. package/dist/server/render-tasks.js +111 -27
  239. package/dist/server/request-context.d.ts +2 -0
  240. package/dist/server/request-context.js +2 -1
  241. package/dist/server/route-table.d.ts +3 -0
  242. package/dist/server/route-table.js +47 -5
  243. package/dist/server/runtime.d.ts +33 -2
  244. package/dist/server/session.d.ts +25 -1
  245. package/dist/server/session.js +18 -4
  246. package/dist/server/settings.js +142 -10
  247. package/dist/server/sweeps.d.ts +4 -0
  248. package/dist/server/sweeps.js +58 -0
  249. package/dist/server/task-views.d.ts +2 -1
  250. package/dist/server/task-views.js +96 -6
  251. package/dist/server/tasks.js +327 -78
  252. package/dist/server/terminals.d.ts +31 -0
  253. package/dist/server/terminals.js +365 -0
  254. package/dist/setup-guide.js +3 -2
  255. package/dist/slack-chat.js +4 -1
  256. package/dist/spend-ui.js +3 -3
  257. package/dist/spend.js +2 -1
  258. package/dist/steer-capabilities.d.ts +32 -0
  259. package/dist/steer-capabilities.js +60 -0
  260. package/dist/steer-controls.d.ts +14 -0
  261. package/dist/steer-controls.js +36 -0
  262. package/dist/steer-status.js +2 -2
  263. package/dist/storage-ui.js +4 -4
  264. package/dist/store-baseline.d.ts +1 -1
  265. package/dist/store-baseline.js +1 -1
  266. package/dist/store.d.ts +440 -15
  267. package/dist/store.js +1072 -52
  268. package/dist/subagent-desk.js +1 -1
  269. package/dist/summary.js +3 -0
  270. package/dist/task-source.d.ts +58 -0
  271. package/dist/task-source.js +93 -0
  272. package/dist/team-leads.js +1 -1
  273. package/dist/teams-chat.js +4 -1
  274. package/dist/teardown-git.d.ts +22 -0
  275. package/dist/teardown-git.js +136 -2
  276. package/dist/telegram-lead.d.ts +3 -0
  277. package/dist/telegram-lead.js +44 -7
  278. package/dist/telegram.d.ts +11 -2
  279. package/dist/telegram.js +51 -17
  280. package/dist/terminal-catalog.d.ts +11 -0
  281. package/dist/terminal-catalog.js +129 -0
  282. package/dist/text-limits.d.ts +2 -0
  283. package/dist/text-limits.js +2 -0
  284. package/dist/tool-broker.d.ts +14 -0
  285. package/dist/tool-broker.js +67 -2
  286. package/dist/toolroll-update-ui.js +2 -2
  287. package/dist/toolroll-update.js +13 -3
  288. package/dist/tools-ui.js +2 -2
  289. package/dist/unfenced-test-support.d.ts +2 -0
  290. package/dist/unfenced-test-support.js +11 -0
  291. package/dist/webhook-limits.d.ts +23 -0
  292. package/dist/webhook-limits.js +30 -0
  293. package/dist/webhook-readers.d.ts +56 -0
  294. package/dist/webhook-readers.js +138 -0
  295. package/dist/webhook-signing.d.ts +27 -0
  296. package/dist/webhook-signing.js +119 -0
  297. package/dist/work-index.d.ts +29 -0
  298. package/dist/work-index.js +16 -2
  299. package/dist/workspace-revision.js +3 -1
  300. package/dist/writes-mode.d.ts +9 -0
  301. package/dist/writes-mode.js +8 -0
  302. package/package.json +15 -2
@@ -1,3 +1,12 @@
1
+ import { steerCapabilityOf } from "../steer-capabilities.js";
2
+ import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
3
+ /** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
4
+ function returnedFlow(value) {
5
+ const match = /^flow:([1-9][0-9]{0,9}):([1-9][0-9]{0,9})$/.exec(value ?? "");
6
+ return match === null ? null : { flow: Number(match[1]), card: Number(match[2]) };
7
+ }
8
+ import { originOf } from "../task-source.js";
9
+ import { ACP_AGENTS, isAcpProvider } from "../acp-agents.js";
1
10
  import { handlersOf } from './handler-registry.js';
2
11
  import { closedQuestionWords } from "../decision.js";
3
12
  import { DEPRECATED_PAGE } from "../deprecations.js";
@@ -21,7 +30,9 @@ import { evidencePack, evidencePackHtml } from "../evidence-pack.js";
21
30
  import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
22
31
  import { computeGaps } from "../gaps.js";
23
32
  import { limitsView } from "../limits-ui.js";
24
- import { readLiveWindow } from "../live.js";
33
+ import { liveWindowLines, liveWindowRecords, readLiveWindow } from "../live.js";
34
+ import { validKeyTail } from "../live-records.js";
35
+ import { runNotes } from "../live-notes.js";
25
36
  import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
26
37
  import { REMOTE_MESSAGES } from "../operate-remote.js";
27
38
  import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
@@ -31,7 +42,7 @@ import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
31
42
  import { levelOfProfile } from "../policy.js";
32
43
  import { authorizedProject, canonicalProject, projectName, rowVisible } from "../project.js";
33
44
  import { fileTaskProposal } from "../proposal.js";
34
- import { isProviderId, validateSpec } from "../provider.js";
45
+ import { isProviderId, PROVIDER_IDS, validateSpec } from "../provider.js";
35
46
  import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
36
47
  import { isQualityMode } from "../quality.js";
37
48
  import { createResultRevision, requestResultChanges } from "../result-actions.js";
@@ -39,7 +50,7 @@ import { structuredHandoffView, terminalDiffView } from "../result-evidence-read
39
50
  import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
40
51
  import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
41
52
  import { isAlive as runnerAlive } from "../runner.js";
42
- import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
53
+ import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
43
54
  import { subagentNames as subagentNamesOf } from "../spend.js";
44
55
  import { verifiedAuthor } from "../store.js";
45
56
  import { runCostWords } from "../summary.js";
@@ -50,10 +61,11 @@ import { agentQuestion, answerAgentQuestion, markAgentSeen } from "../agent-boar
50
61
  import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
51
62
  import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
52
63
  import { requestContext } from "./request-context.js";
53
- import { oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
64
+ import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
54
65
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
55
- import { boardBody, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, tasksPage, workPage } from "./render-pages.js";
56
- import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
66
+ import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, tasksPage, workPage } from "./render-pages.js";
67
+ import { runEfficiencyOf } from "../run-efficiency.js";
68
+ import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
57
69
  import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
58
70
  import {} from "./session.js";
59
71
  export function createTasksHandlers(runtime) {
@@ -299,6 +311,7 @@ export function createTasksHandlers(runtime) {
299
311
  approvalDigest,
300
312
  deliverable: ref?.deliverable ?? "branch",
301
313
  route,
314
+ origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
302
315
  csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
303
316
  }));
304
317
  }
@@ -386,7 +399,10 @@ export function createTasksHandlers(runtime) {
386
399
  });
387
400
  });
388
401
  const laneCards = [...cards, ...unverifiedCards];
389
- const body = boardBody({ cards: laneCards, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
402
+ // The worker strip: the workers this board's work can go to — bound to
403
+ // this project, or (rolled up) to any project the ceiling admits.
404
+ const workers = boardWorkers(store.listRunners(), { all, project, visible });
405
+ const body = boardBody({ cards: laneCards, workers, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
390
406
  if (url.searchParams.get("fragment") === "1") {
391
407
  // The live region alone — the in-page swapper's diet. Same auth,
392
408
  // same ceiling, no shell, no scripts (finding 2).
@@ -410,9 +426,73 @@ export function createTasksHandlers(runtime) {
410
426
  const regionBody = html `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
411
427
  return sendScreen(response, 200, screen("board", regionBody, {
412
428
  chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
413
- functional: { script: regionScript("board-region", "1"), fetches: true },
429
+ functional: { script: regionScript("board-region", "1", undefined, { animate: true }) + boardTickScript(), fetches: true },
414
430
  }));
415
431
  }
432
+ /** A completed (or attempted) row as the review list ranks it: its proof reasons, CI standing, and the assignment
433
+ * when that assignment's receipt is this very row. */
434
+ function reviewRowOf(row, now) {
435
+ const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
436
+ return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
437
+ assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
438
+ }
439
+ /** The result a link names (the task's own result, or the exact run `namedRun` of it): its completed row when the
440
+ * task is done (`inQueue` when the review list already holds it), else that attempt's own result (a failed or stopped
441
+ * build has a result page too), with what went wrong and Retry / Accept anyway as the review page offers them.
442
+ * Shared by /review and the task page's Result tab (docs/plans/one-page-task-and-agent.md). */
443
+ function selectResultFor(wantedId, namedRun, project, who, now, inQueue = null) {
444
+ const csrf = who.via === "cookie" ? who.session.csrf : "";
445
+ const beyond = inQueue !== null ? null : completedRowFor(wantedId, project);
446
+ const beyondRow = beyond === null ? null : reviewRowOf(beyond, now);
447
+ const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
448
+ // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
449
+ const attempt = (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
450
+ const attemptRow = attempt === null ? null : reviewRowOf(attempt, now);
451
+ const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
452
+ const selected = chosen === null ? null : reviewCockpitViewOf(chosen, who, now);
453
+ // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
454
+ // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
455
+ // a reason.
456
+ const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
457
+ const failedTask = selected === null ? null : store.getTask(selected.taskId);
458
+ if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
459
+ const attemptRun = store.getRun(selected.run.id);
460
+ const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
461
+ const latest = latestFinishedAttempt(runs);
462
+ // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
463
+ const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
464
+ ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
465
+ // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
466
+ const family = delivered ? familyOf(selected.taskId) : null;
467
+ const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
468
+ runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
469
+ ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
470
+ if (attemptRun !== null) {
471
+ // A delivered result didn't fail itself: the task's failure is its latest attempt's.
472
+ const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
473
+ selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
474
+ }
475
+ }
476
+ return { chosen, attemptRow, beyond, selected };
477
+ }
478
+ /** Whether `run` is a result selectResultFor shows for version `taskId`: a finished attempt that didn't deliver (failed,
479
+ * stopped, refused…), or the delivered one that is the version's own result — its completed row's, or, for a failed
480
+ * version, its latest finished attempt. An older delivered build is not (its version's result is the newer one). */
481
+ function resultRunAdmissible(taskId, run, project) {
482
+ if ((run.role !== "builder" && run.role !== "scout") || (run.finishedAt === null && run.outcome === null) || runIsLive(run))
483
+ return false;
484
+ if (run.outcome !== "built" && run.outcome !== "no-change")
485
+ return true;
486
+ const task = store.getTask(taskId);
487
+ if (task?.state === "failed")
488
+ return latestFinishedAttempt(store.runsFor(run.taskRef).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null)))?.id === run.id;
489
+ return completedRowFor(taskId, project)?.runId === run.id;
490
+ }
491
+ /** A run's own task and its family's root: the task pages a result form may return to. */
492
+ function runTasks(taskRef) {
493
+ const task = store.externalIdFor(taskRef);
494
+ return task === null ? [] : [task, familyOf(task)?.root.id ?? task];
495
+ }
416
496
  async function showReview(ctx) {
417
497
  const { url, who, response, now, project, chosenProject } = ctx;
418
498
  // The review cockpit (Priority 5): every visible COMPLETED task,
@@ -423,11 +503,7 @@ export function createTasksHandlers(runtime) {
423
503
  // stored verdict, and the sealed patch downloads exactly as stored.
424
504
  // Admission binds BEFORE the SQL limit (the done page's own rule),
425
505
  // and every row is re-proved against the ceiling before ranking.
426
- const resultRow = (row) => {
427
- const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
428
- return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
429
- assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
430
- };
506
+ const resultRow = (row) => reviewRowOf(row, now);
431
507
  const rows = familiesInView(project, { states: ["done"], limit: REVIEW_QUEUE_CAP }).flatMap(family => {
432
508
  const row = completedRowFor(family.current.id, project);
433
509
  return row === null ? [] : [{ ...resultRow(row), title: family.root.title }];
@@ -441,50 +517,23 @@ export function createTasksHandlers(runtime) {
441
517
  // directly — re-proved as done, in this project, and inside the
442
518
  // ceiling — so an old receipt never reads as unfinished or foreign.
443
519
  // The ranked queue itself stays bounded and says so.
444
- const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
445
- const beyondRow = beyond === null ? null : resultRow(beyond);
446
- const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
447
520
  const expectedRun = url.searchParams.get("run");
448
521
  const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
449
- // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
450
- const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
451
- const attemptRow = attempt === null ? null : resultRow(attempt);
452
- const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
453
- const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
454
522
  const csrf = who.via === "cookie" ? who.session.csrf : "";
523
+ const picked = wantedId === null ? null : selectResultFor(wantedId, namedRun, project, who, now, inQueue);
524
+ const chosen = picked?.chosen ?? null, attemptRow = picked?.attemptRow ?? null, beyond = picked?.beyond ?? null;
525
+ const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
455
526
  // An exact link to a result this person can't see (or that never existed) reads the same either way.
456
527
  if (expectedRun !== null && wantedId !== null && chosen === null)
457
528
  return refuse(response, who, 404, "No such result in your projects.", "/work");
458
529
  if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
459
530
  return sendScreen(response, 409, screen("Result changed", html `<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>`, { chrome: chromeFor(project, "runs") }));
460
531
  }
461
- const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
462
- // Opening a run's own result page is viewing it: it leaves this person's unread Done on the Agents board.
463
- if (wantedId !== null && selected?.run != null)
532
+ const selected = picked !== null ? picked.selected : selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
533
+ // Opening a run's own result page is viewing it: it leaves this person's unread Done on the Agents board — but only
534
+ // once the run has finished (cockpit §3): a page shown while it was still running saw no finished version.
535
+ if (wantedId !== null && selected?.run != null && store.getRun(selected.run.id)?.finishedAt != null)
464
536
  markAgentSeen(store, who.name, selected.run.id, now);
465
- // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
466
- // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
467
- // a reason.
468
- const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
469
- const failedTask = selected === null ? null : store.getTask(selected.taskId);
470
- if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
471
- const attemptRun = store.getRun(selected.run.id);
472
- const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
473
- const latest = latestFinishedAttempt(runs);
474
- // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
475
- const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
476
- ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
477
- // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
478
- const family = delivered ? familyOf(selected.taskId) : null;
479
- const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
480
- runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
481
- ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
482
- if (attemptRun !== null) {
483
- // A delivered result didn't fail itself: the task's failure is its latest attempt's.
484
- const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
485
- selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
486
- }
487
- }
488
537
  const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
489
538
  queue: ranked,
490
539
  queueCap: REVIEW_QUEUE_CAP,
@@ -570,9 +619,42 @@ export function createTasksHandlers(runtime) {
570
619
  if (version !== null && !family.versions.some(one => one.id === version))
571
620
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
572
621
  // A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
573
- if (task.taskId !== family.root.id)
574
- return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
575
- return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
622
+ if (task.taskId !== family.root.id) {
623
+ // Everything else the address asked for (its tab, result run and view, a form's feedback) rides along.
624
+ const kept = new URLSearchParams(url.searchParams);
625
+ kept.delete("version");
626
+ return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${kept.size === 0 ? "" : `&${kept.toString()}`}`);
627
+ }
628
+ // One page per task (docs/plans/one-page-task-and-agent.md): the tab named, and the Result tab's result — the shown
629
+ // version's own, or the exact run `run=` names, which must be a result of that very version.
630
+ const tabWord = url.searchParams.get("tab");
631
+ const tab = tabWord === "result" || tabWord === "plan" ? tabWord : "activity";
632
+ const runWord = url.searchParams.get("run");
633
+ const named = runWord === null ? null : /^[1-9][0-9]{0,14}$/.test(runWord) ? Number(runWord) : -1;
634
+ if (named === -1)
635
+ return refuse(response, who, 404, "No such result for this task.", taskHref(family.root.id));
636
+ // A run named without a version is shown with the version it belongs to: a saved link to a result outlives a newer
637
+ // revision becoming current. A run of no version of this family is no result of it.
638
+ const owner = named === null || version !== null ? null : (() => { const ref = store.getRun(named)?.taskRef; const id = ref === undefined ? null : store.externalIdFor(ref); return id !== null && family.versions.some(one => one.id === id) ? id : null; })();
639
+ const shown = version ?? owner ?? family.current.id;
640
+ const picked = selectResultFor(shown, named, ctx.project, who, ctx.now);
641
+ if (named !== null && picked.chosen?.runId !== named)
642
+ return refuse(response, who, 404, "No such result for this task.", taskHref(family.root.id));
643
+ const base = `${taskHref(family.root.id)}?${shown !== family.current.id ? `version=${encodeURIComponent(shown)}&` : ""}tab=result`;
644
+ const csrf = who.via === "cookie" ? who.session.csrf : "";
645
+ const result = picked.selected === null ? null : (() => {
646
+ const parts = reviewCockpitDetailParts(picked.selected, csrf, url.searchParams.get("noted") !== null, who.via === "cookie" && who.role === "approver", parseResultTab(url.searchParams.get("section")), who.name, { kind: "task", base });
647
+ // Current: the result the task itself would show (its latest), however the address named it — the header then
648
+ // owns its status and ceremonies, so no form is ever on the page twice.
649
+ const latest = named === null ? picked.chosen?.runId ?? null : selectResultFor(shown, null, ctx.project, who, ctx.now).chosen?.runId ?? null;
650
+ return { html: parts.html, selected: parts.selected, current: shown === family.current.id && latest !== null && latest === picked.chosen?.runId };
651
+ })();
652
+ // The attempts the picker offers: exactly the runs this page would show for their version (the same rule as above).
653
+ const admissible = new Set(family.versions.flatMap(one => {
654
+ const ref = store.lookupRef(one.id);
655
+ return ref === null ? [] : store.runsFor(ref.id).filter(run => resultRunAdmissible(one.id, run, ctx.project)).map(run => run.id);
656
+ }));
657
+ return taskScreen(response, who, shown, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan", { tab, result, admissible });
576
658
  }
577
659
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
578
660
  async function showTaskEvidence(ctx) {
@@ -644,23 +726,28 @@ export function createTasksHandlers(runtime) {
644
726
  return respond(response, peeked.status, "text/html; charset=utf-8", peeked.body);
645
727
  }
646
728
  if (url.searchParams.get("fragment") === "transcript") {
647
- // The live transcript window (arc 1): raw sanitized TEXT as JSON —
648
- // the sink is textContent, never innerHTML. Guards, enumerated:
649
- // cookie session; run visible (proved above); this machine's
650
- // runner asserted; byte offsets validated; the file read through
651
- // its own descriptor with the exact numeric-id name. Display
652
- // state, not evidence — and never cached.
729
+ // The live conversation's window (arc 1; live-conversation.md §3–4):
730
+ // JSON over sanitized text or records — the sinks are textContent and
731
+ // React text, never innerHTML. Guards, enumerated: cookie session; run
732
+ // visible (proved above); this machine's runner asserted AND owning
733
+ // the run; byte offsets validated; the file read through its own
734
+ // descriptor with the exact numeric-id name, and its generation
735
+ // matched. Display state, not evidence — and never cached.
653
736
  response.setHeader("cache-control", "no-store");
654
737
  if (who.via !== "cookie")
655
738
  return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
656
- if (options.localRunner === undefined) {
657
- return respond(response, 200, "application/json", JSON.stringify({ error: "off" }));
739
+ const records = url.searchParams.get("shape") === "records";
740
+ // Notes come from the store, not the file: they show even where the transcript cannot be read.
741
+ const notesOf = () => (records ? runNotes(store, found, who.name, now) : {});
742
+ if (options.localRunner === undefined || found.runner !== options.localRunner) {
743
+ return respond(response, 200, "application/json", JSON.stringify({ error: "off", ...notesOf() }));
658
744
  }
659
745
  const fromRaw = url.searchParams.get("from") ?? "0";
660
746
  const from = /^[0-9]{1,15}$/.test(fromRaw) ? Number(fromRaw) : Number.NaN;
661
747
  if (!Number.isSafeInteger(from) || from < 0) {
662
748
  return respond(response, 400, "application/json", JSON.stringify({ error: "offset" }));
663
749
  }
750
+ const sawGen = url.searchParams.get("gen");
664
751
  const live = runIsLive(found);
665
752
  // The final drain: a finished run's tail — including a torn last
666
753
  // line — may be read until the sweep removes the file.
@@ -671,15 +758,35 @@ export function createTasksHandlers(runtime) {
671
758
  }
672
759
  // Missing or unreadable: nothing to show. Final only when the
673
760
  // run can never write again.
674
- return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: !live }));
761
+ // Nothing was read, so the key state the cursor carried is handed straight back.
762
+ const inKey = url.searchParams.get("inkey") === "1";
763
+ const keyTail = validKeyTail(url.searchParams.get("ktail") ?? "") ? url.searchParams.get("ktail") ?? "" : "";
764
+ const empty = records ? { records: [], nextOffset: from, gen: sawGen, inKey, keyTail, final: !live } : { text: "", nextOffset: from, inKey, keyTail, final: !live };
765
+ return respond(response, 200, "application/json", JSON.stringify({ ...empty, ...notesOf() }));
766
+ }
767
+ // An offset belongs to the file it was read from: another file starts over, visibly.
768
+ if (sawGen !== null && sawGen !== "" && sawGen !== window.gen) {
769
+ return respond(response, 409, "application/json", JSON.stringify({ error: "replaced" }));
675
770
  }
676
771
  // Re-proof after the read (peek discipline): the run row still says
677
- // what admission said, or nothing is shown.
772
+ // what admission said — visible, and still this machine's — or nothing is shown.
678
773
  const again = store.getRun(found.id);
679
- if (again === null || !runVisible(again)) {
680
- return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: true }));
774
+ if (again === null || !runVisible(again) || again.runner !== options.localRunner) {
775
+ const inKey = url.searchParams.get("inkey") === "1";
776
+ const keyTail = validKeyTail(url.searchParams.get("ktail") ?? "") ? url.searchParams.get("ktail") ?? "" : "";
777
+ return respond(response, 200, "application/json", JSON.stringify(records ? { records: [], nextOffset: from, gen: window.gen, inKey, keyTail, final: true } : { text: "", nextOffset: from, inKey, keyTail, final: true }));
778
+ }
779
+ const final = !live && window.eof;
780
+ // A key block split across reads stays hidden: the cursor carries whether the last read ended inside one.
781
+ const tailRaw = url.searchParams.get("ktail") ?? "";
782
+ const keys = { inKey: url.searchParams.get("inkey") === "1", tail: validKeyTail(tailRaw) ? tailRaw : "" };
783
+ if (records) {
784
+ const shown = liveWindowRecords(window, keys);
785
+ return respond(response, 200, "application/json", JSON.stringify({ records: shown, nextOffset: window.nextOffset, gen: window.gen, inKey: keys.inKey, keyTail: keys.tail ?? "", final, ...notesOf() }));
681
786
  }
682
- return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
787
+ // The plain words, whatever the file's format; the offset moves by the bytes read even when nothing shows.
788
+ const lines = liveWindowLines(window, keys);
789
+ return respond(response, 200, "application/json", JSON.stringify({ text: lines.length === 0 ? "" : `${lines.join("\n")}\n`, nextOffset: window.nextOffset, gen: window.gen, inKey: keys.inKey, keyTail: keys.tail ?? "", final }));
683
790
  }
684
791
  // One result page (2026-10-02): a builder's finished result opens on
685
792
  // its task's result page, titled with the task. The raw run record
@@ -1160,7 +1267,7 @@ export function createTasksHandlers(runtime) {
1160
1267
  ? `Something from build #${run.id} may still be running, so it can't be confirmed stopped yet.` : `Build #${run.id} can't be confirmed stopped.`, 409);
1161
1268
  }
1162
1269
  bustBadge();
1163
- const to = resultReturnTarget(body.get("return"), run.id);
1270
+ const to = resultReturnTarget(body.get("return"), run.id, runTasks(run.taskRef));
1164
1271
  return redirect(response, body.get("return") ? to : back);
1165
1272
  }
1166
1273
  async function taskMerge(ctx) {
@@ -1195,6 +1302,28 @@ export function createTasksHandlers(runtime) {
1195
1302
  return noPage(who, response);
1196
1303
  return taskMutation(response, who, act.taskId, verb, posted, now);
1197
1304
  };
1305
+ // Cockpit E: Show in Finder — the checkout proved again right now (live, this machine's runner, inside the pool),
1306
+ // then Finder asked to reveal it with a fixed argv. Nothing runs in it; a Mac only.
1307
+ async function runReveal(ctx) {
1308
+ const { url, who, response, now, posted } = ctx;
1309
+ const asked = /^\/r\/([0-9]{1,15})\/reveal$/.exec(url.pathname);
1310
+ if (asked === null)
1311
+ return noPage(who, response);
1312
+ readForm(posted, CONSOLE_FORMS.runReveal);
1313
+ const id = Number(asked[1]);
1314
+ const found = store.getRun(id);
1315
+ if (found === null || !visible(taskRepoOf(found.taskRef)))
1316
+ return refuse(response, who, 404, "no such run");
1317
+ if (store.isDemo())
1318
+ return refuse(response, who, 403, "the demo authorizes nothing");
1319
+ const proof = localCheckoutOf(store, id, { runner: options.localRunner, poolRoot: options.poolRoot }, now);
1320
+ if (!proof.ok)
1321
+ return redirect(response, `/agents?run=${id}&said=reveal-refused`);
1322
+ if (!canReveal(options.revealPath !== undefined))
1323
+ return redirect(response, `/agents?run=${id}&said=reveal-mac-only`);
1324
+ const shown = await (options.revealPath ?? revealInFinder)(proof.path);
1325
+ return redirect(response, `/agents?run=${id}&said=${shown ? "revealed" : "reveal-failed"}`);
1326
+ }
1198
1327
  async function runNote(ctx) {
1199
1328
  const { url, who, response, now, posted } = ctx;
1200
1329
  const noted = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
@@ -1235,9 +1364,12 @@ export function createTasksHandlers(runtime) {
1235
1364
  // the chat's result view, or the run page — validated to those exact
1236
1365
  // shapes. A refusal sends them back THERE, where the browser has kept
1237
1366
  // their draft, so a failed submission is recoverable in place.
1238
- const returnTo = resultReturnTarget(body.get("return"), id);
1367
+ const returnTo = resultReturnTarget(body.get("return"), id, runTasks(found.taskRef));
1239
1368
  const selectedTab = parseResultTab(body.get("tab"));
1240
- const back = selectedTab === "summary" ? returnTo : `${returnTo}${returnTo.includes("?") ? "&" : "?"}tab=${selectedTab}`;
1369
+ // A task page's Result tab keeps the result's own view in `section=` (its `tab` is the task's).
1370
+ const viewParam = returnTo.startsWith("/t/") ? "section" : "tab";
1371
+ const back = selectedTab === "summary" ? returnTo
1372
+ : `${returnTo.replace(/&section=(?:summary|changes|checks)$/, "")}${returnTo.includes("?") ? "&" : "?"}${viewParam}=${selectedTab}`;
1241
1373
  if (body.get("intent") === "revise") {
1242
1374
  const result = requestResultChanges(store, evidenceRoot, {
1243
1375
  run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
@@ -1337,7 +1469,7 @@ export function createTasksHandlers(runtime) {
1337
1469
  if (found === null || !visible(taskRepoOf(found.taskRef))) {
1338
1470
  return refuse(response, who, 404, "no such run");
1339
1471
  }
1340
- const back = resultReturnTarget(body.get("return"), id);
1472
+ const back = resultReturnTarget(body.get("return"), id, runTasks(found.taskRef));
1341
1473
  const result = createResultRevision(store, evidenceRoot, {
1342
1474
  run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1343
1475
  repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
@@ -1359,8 +1491,7 @@ export function createTasksHandlers(runtime) {
1359
1491
  if (found === null || !visible(taskRepoOf(found.taskRef)))
1360
1492
  return refuse(response, who, 404, "no such run");
1361
1493
  // The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
1362
- const runTask = store.externalIdFor(found.taskRef);
1363
- const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
1494
+ const back = resultReturnTarget(body.get("return"), id, runTasks(found.taskRef));
1364
1495
  if (who.via !== "cookie")
1365
1496
  return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
1366
1497
  if (op === "checks") {
@@ -1615,6 +1746,72 @@ export function createTasksHandlers(runtime) {
1615
1746
  if (who.via !== "cookie") {
1616
1747
  return refuse(response, who, 403, "steering is a browser session's act");
1617
1748
  }
1749
+ // The same gates as a pause or a stop (the route's act scope already refuses a viewer; this says why).
1750
+ if (who.role !== "approver")
1751
+ return taskScreen(response, who, taskId, "your login can watch — sending an agent a note is an approver's act", 403);
1752
+ if (store.isDemo())
1753
+ return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
1754
+ // From the Agents view, the note is for the exact run on screen (live-conversation.md §6): filed only while that
1755
+ // run is still the task's live build, once per draft key. Nothing is filed on a refusal; the view keeps the draft.
1756
+ // An unsent note from an attempt that ended, sent on to the task's next attempt from that agent's page: filed
1757
+ // task-level, back to the same agent with its draft key so only a confirmed send clears the draft.
1758
+ if (body.has("after")) {
1759
+ const afterRaw = body.get("after") ?? "";
1760
+ const keyRaw = body.get("key") ?? "";
1761
+ const ended = /^[1-9][0-9]{0,14}$/.test(afterRaw) ? store.getRun(Number(afterRaw)) : null;
1762
+ const afterBack = body.get("return");
1763
+ const afterFlow = returnedFlow(afterBack);
1764
+ if (ended === null || ended.taskRef !== ref.id || !/^[A-Za-z0-9-]{8,64}$/.test(keyRaw) || body.has("run") || (afterBack !== null && afterBack !== "task" && afterFlow === null)) {
1765
+ return taskScreen(response, who, taskId, "that note did not say which agent it came from, so nothing was sent", 400);
1766
+ }
1767
+ // The same receipt as a send to that run: a draft already filed while it ran comes back as sent, not twice.
1768
+ const filed = store.fileSteerNoteForRun(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now, { run: ended.id, key: keyRaw, onward: true });
1769
+ // From the task page or a flow card: back there with the draft's key (only that draft clears), or the refusal in words.
1770
+ if (afterBack !== null) {
1771
+ if (filed.ok)
1772
+ return redirect(response, afterFlow !== null ? `/flows/${afterFlow.flow}?card=${afterFlow.card}&sent=${keyRaw}` : `${taskHref(taskId)}?sent=${keyRaw}#steering`);
1773
+ return taskScreen(response, who, taskId, filed.reason === "task-finished" ? "this task is finished, so there's no next attempt to send it to" : filed.reason === "key-reused" ? "that form was already used — reload and send it again" : "that note will not store", 409);
1774
+ }
1775
+ if (filed.ok)
1776
+ return redirect(response, `/agents?run=${ended.id}&said=steered-next&sent=${keyRaw}`);
1777
+ return redirect(response, `/agents?run=${ended.id}&said=${filed.reason === "task-finished" ? "steer-next-finished" : filed.reason === "key-reused" ? "steer-stale" : "steer-invalid"}`);
1778
+ }
1779
+ // A form that asks for a run but names none properly is refused — never quietly filed unbound.
1780
+ if (body.get("return") === "agents" || body.get("return") === "task" || returnedFlow(body.get("return")) !== null || body.has("run") || body.has("key")) {
1781
+ const runRaw = body.get("run") ?? "";
1782
+ const keyRaw = body.get("key") ?? "";
1783
+ const flowBack = returnedFlow(body.get("return"));
1784
+ const from = flowBack !== null ? "task" : body.get("return");
1785
+ if ((from !== "agents" && from !== "task") || !/^[1-9][0-9]{0,14}$/.test(runRaw) || !/^[A-Za-z0-9-]{8,64}$/.test(keyRaw)) {
1786
+ return taskScreen(response, who, taskId, "that note did not say which agent it is for, so nothing was sent", 400);
1787
+ }
1788
+ const shown = store.getRun(Number(runRaw));
1789
+ if (shown === null || shown.taskRef !== ref.id)
1790
+ return taskScreen(response, who, taskId, "that run is not part of this task", 404);
1791
+ // An attempt that takes no notes (a planner, a scout, a reviewer, one repairing its handoff) is never filed one:
1792
+ // it would never read it (cockpit §4).
1793
+ const capability = steerCapabilityOf(store, shown.id, now);
1794
+ if (capability !== null && capability.send === null) {
1795
+ return from === "agents" ? redirect(response, `/agents?run=${shown.id}&said=steer-no-notes`) : taskScreen(response, who, taskId, capability.words, 409);
1796
+ }
1797
+ const bound = store.fileSteerNoteForRun(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now, { run: shown.id, key: keyRaw });
1798
+ // From the task page: back to its steering card, or the refusal in words (the page keeps nothing to restore).
1799
+ if (from === "task") {
1800
+ // Back where it was written (the task page, or a flow card's task), with its draft's key: only that draft clears.
1801
+ if (bound.ok)
1802
+ return redirect(response, flowBack !== null ? `/flows/${flowBack.flow}?card=${flowBack.card}&sent=${keyRaw}` : `${taskHref(taskId)}?sent=${keyRaw}#steering`);
1803
+ return taskScreen(response, who, taskId, bound.reason === "attempt-ended" || bound.reason === "task-finished" ? "that attempt ended before your note, so nothing was sent — reload and send it to the current one"
1804
+ : bound.reason === "invalid-note" ? "that note will not store" : "that form was already used — reload and send it again", 409);
1805
+ }
1806
+ // The draft's own key comes back: only that draft is cleared from the box.
1807
+ if (bound.ok)
1808
+ return redirect(response, `/agents?run=${shown.id}&said=steered&sent=${keyRaw}`);
1809
+ if (bound.reason === "attempt-ended" || bound.reason === "task-finished")
1810
+ return redirect(response, `/agents?run=${shown.id}&said=steer-ended`);
1811
+ if (bound.reason === "invalid-note")
1812
+ return redirect(response, `/agents?run=${shown.id}&said=steer-invalid`);
1813
+ return redirect(response, `/agents?run=${shown.id}&said=steer-stale`);
1814
+ }
1618
1815
  // The session IS the verified principal here — cookie + CSRF proved it.
1619
1816
  const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now);
1620
1817
  if (!filed.ok) {
@@ -1625,10 +1822,6 @@ export function createTasksHandlers(runtime) {
1625
1822
  : "no such task";
1626
1823
  return taskScreen(response, who, taskId, said, 400);
1627
1824
  }
1628
- // From the Agents board, back to the same agent (allow-listed: a run of this task, never a URL from the form).
1629
- const run = Number(body.get("run") ?? "");
1630
- if (body.get("return") === "agents" && Number.isSafeInteger(run) && store.getRun(run)?.taskRef === ref.id)
1631
- return redirect(response, `/agents?run=${run}&said=steered`);
1632
1825
  return redirect(response, taskHref(taskId));
1633
1826
  }
1634
1827
  case "reply": {
@@ -1999,7 +2192,7 @@ export function createTasksHandlers(runtime) {
1999
2192
  return taskScreen(response, who, taskId, "nothing to change about the agents", 400);
2000
2193
  if (phaseGiven !== null) {
2001
2194
  if (providerGiven === null || !isProviderId(providerGiven))
2002
- return taskScreen(response, who, taskId, "provider is claude, codex, openrouter, or gemini", 400);
2195
+ return taskScreen(response, who, taskId, `provider is one of ${PROVIDER_IDS.join(", ")}`, 400);
2003
2196
  if (modelGiven === "")
2004
2197
  return taskScreen(response, who, taskId, "name the exact model id — approvals bind exact agents", 400);
2005
2198
  const valid = validateSpec({ provider: providerGiven, model: modelGiven });
@@ -2007,6 +2200,8 @@ export function createTasksHandlers(runtime) {
2007
2200
  return taskScreen(response, who, taskId, `agents not changed: ${valid.problem}`, 400);
2008
2201
  if (phaseGiven === "review" && providerGiven === "gemini")
2009
2202
  return taskScreen(response, who, taskId, "gemini cannot review yet — choose claude or codex", 400);
2203
+ if ((phaseGiven === "plan" || phaseGiven === "review") && isAcpProvider(providerGiven))
2204
+ return taskScreen(response, who, taskId, `${ACP_AGENTS[providerGiven].name} builds and repairs only — choose another agent to ${phaseGiven}`, 400);
2010
2205
  }
2011
2206
  // The configured-choice proof lives INSIDE the edit transaction
2012
2207
  // (`configured`): the pair is held to the role's choices under the
@@ -2186,7 +2381,9 @@ export function createTasksHandlers(runtime) {
2186
2381
  if (!consumeApprovalNonce(nonce, who.name, taskId, digest)) {
2187
2382
  return approvalProblem("that approval form is stale — read it again", 409);
2188
2383
  }
2189
- if (token === "" && !hasFreshIdentitySignIn(who.name)) {
2384
+ // An empty password stands for an explicit identity-provider re-check not yet spent, or (cockpit B2) a password
2385
+ // remembered on this session; approve() decides whether these terms may stand on a remembered one.
2386
+ if (token === "" && !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null) {
2190
2387
  return approvalProblem("approval requires your password, typed again", 400);
2191
2388
  }
2192
2389
  const scopeRow = store.getScope(taskId);
@@ -2208,7 +2405,8 @@ export function createTasksHandlers(runtime) {
2208
2405
  return approvalProblem(`approval is blocked: ${view.problem}`, 409);
2209
2406
  }
2210
2407
  }
2211
- const approved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
2408
+ // A re-check is spent only if this yes is kept (sealed, or a protected-work vote recorded).
2409
+ const approved = spendingOnlyOnSuccess(() => withFreshness("approve-standard", () => approveScope(store, taskId, who.name, now, scopeRow.digest, token)), result => !result.ok && !(result.reason === "second-approver" && !result.already));
2212
2410
  if (!approved.ok) {
2213
2411
  // v102: a first yes on protected work is recorded, not refused — the page says who else is needed.
2214
2412
  if (approved.reason === "second-approver")
@@ -2218,6 +2416,20 @@ export function createTasksHandlers(runtime) {
2218
2416
  const status = approved.reason === "changed" || approved.reason === "unrouted" ? 409 : 403;
2219
2417
  return approvalProblem(approveRefusalWords(approved.reason), status);
2220
2418
  }
2419
+ // Approve & build again (the plan was rewritten after a build): the same yes requeues, only for the build the
2420
+ // sheet named, and only while it is still the task's latest result, made to an earlier plan than this one.
2421
+ const rebuildRaw = body.get("rebuild") ?? "";
2422
+ if (rebuildRaw !== "") {
2423
+ const named = /^[0-9]{1,15}$/.test(rebuildRaw) ? store.getRun(Number(rebuildRaw)) : null;
2424
+ // Built to an earlier plan than the one just approved (the scope this approval sealed, scopeRow.digest);
2425
+ // requeueTask re-proves, in its own transaction, that it is still the latest result and that scope approved.
2426
+ const offered = named != null && named.taskRef === ref.id && named.role === "builder" && !!named.scopeDigest && named.scopeDigest !== scopeRow.digest;
2427
+ const requeued = offered ? store.requeueTask(taskId, who.name, now, { run: named.id, scopeDigest: scopeRow.digest }) : null;
2428
+ if (requeued === null || (!requeued.ok && requeued.reason === "moved"))
2429
+ return approvalProblem("Approved. The task changed while this page was open, so it wasn't built again — check its status.", 200);
2430
+ if (!requeued.ok)
2431
+ return approvalProblem(`Approved, but building again didn't start (${requeued.reason}). Use Build again.`, 200);
2432
+ }
2221
2433
  return redirect(response, approvalBack);
2222
2434
  }
2223
2435
  case "accept-proof": {
@@ -2297,7 +2509,8 @@ export function createTasksHandlers(runtime) {
2297
2509
  const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
2298
2510
  if (!completed.ok)
2299
2511
  return taskScreen(response, who, taskId, completed.message, 409);
2300
- return redirect(response, reviewHref(taskId, Number(namedRun)));
2512
+ // Back where it was completed (the task page's Result tab, or the result page), as Accept and finish does.
2513
+ return redirect(response, body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
2301
2514
  }
2302
2515
  case "retry-review": {
2303
2516
  return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
@@ -2331,6 +2544,12 @@ export function createTasksHandlers(runtime) {
2331
2544
  return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, 409);
2332
2545
  }
2333
2546
  const back = body.get("return") ?? "";
2547
+ // From the Agents board: back to the same agent, saying what happens now.
2548
+ if (back === "agents")
2549
+ return redirect(response, `/agents?run=${Number(named)}&said=stopping`);
2550
+ const stopFlow = returnedFlow(back);
2551
+ if (stopFlow !== null)
2552
+ return redirect(response, `/flows/${stopFlow.flow}?card=${stopFlow.card}`);
2334
2553
  return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
2335
2554
  }
2336
2555
  case "pause": {
@@ -2365,6 +2584,26 @@ export function createTasksHandlers(runtime) {
2365
2584
  return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, asked.reason === "invalid-note" ? 400 : 409);
2366
2585
  }
2367
2586
  const back = body.get("return") ?? "";
2587
+ // A note that rode along came from a composer's draft: its key comes back, so only that draft clears.
2588
+ const keyRaw = body.get("key") ?? "";
2589
+ // Only a note this request actually stored clears its draft: a repeated request answers with the note it stored
2590
+ // the first time, and a draft edited since is not that note.
2591
+ const storedId = "note" in asked && typeof asked.note === "number" ? asked.note : null;
2592
+ const stored = storedId === null ? null : store.handle.prepare("SELECT note FROM task_steer WHERE id = ?").get(storedId)?.["note"];
2593
+ const sent = note !== "" && /^[A-Za-z0-9-]{8,64}$/.test(keyRaw) && typeof stored === "string" && stored.trim() === note ? keyRaw : null;
2594
+ // A note that rode along but is not the one this request stored (a reused form): said plainly, the draft kept.
2595
+ if (note !== "" && (typeof stored !== "string" || stored.trim() !== note)) {
2596
+ if (back === "agents")
2597
+ return redirect(response, `/agents?run=${Number(named)}&said=pause-note-stale`);
2598
+ return taskScreen(response, who, taskId, "that form was already used, so your note was not sent — it is still in the box; send it again", 409);
2599
+ }
2600
+ if (back === "agents")
2601
+ return redirect(response, `/agents?run=${Number(named)}&said=${body.get("mode") === "resume" ? "resuming" : note === "" ? "pausing" : "pausing-with-note"}${sent === null ? "" : `&sent=${sent}`}`);
2602
+ const pauseFlow = returnedFlow(back);
2603
+ if (pauseFlow !== null)
2604
+ return redirect(response, `/flows/${pauseFlow.flow}?card=${pauseFlow.card}${sent === null ? "" : `&sent=${sent}`}`);
2605
+ if (back === "task")
2606
+ return redirect(response, `${taskHref(taskId)}${sent === null ? "" : `?sent=${sent}`}#task-control`);
2368
2607
  return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
2369
2608
  }
2370
2609
  case "resume-arm": {
@@ -2523,7 +2762,7 @@ export function createTasksHandlers(runtime) {
2523
2762
  handoff: run?.handoff ?? null,
2524
2763
  costUsd: run?.costUsd ?? null,
2525
2764
  provider: run?.provider ?? null,
2526
- authMode: run?.authMode === "subscription" || run?.authMode === "api-key" ? run.authMode : null,
2765
+ authMode: run?.authMode === "subscription" || run?.authMode === "api-key" || run?.authMode === "agent-login" ? run.authMode : null,
2527
2766
  ranMinutes: run === null || run.finishedAt === null
2528
2767
  ? null
2529
2768
  : Math.max(1, Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 60_000)),
@@ -2598,6 +2837,15 @@ export function createTasksHandlers(runtime) {
2598
2837
  summary: detail.handoff?.conclusion ?? run.handoff,
2599
2838
  },
2600
2839
  detail,
2840
+ // How it ran: derived from the run's own records once it has finished, display only.
2841
+ efficiency: (() => { try {
2842
+ return runEfficiencyOf(store, evidenceRoot, run.id, now);
2843
+ }
2844
+ catch {
2845
+ return null;
2846
+ } })(),
2847
+ // What its network was and what its proxy saw (egress.ts), display only.
2848
+ network: store.runNetwork(run.id),
2601
2849
  reviewRetry: store.reviewRetryStateOf(run.id),
2602
2850
  review: reviewFactsFor(run.id),
2603
2851
  notes: store.notesForRun(run.id),
@@ -2793,6 +3041,7 @@ export function createTasksHandlers(runtime) {
2793
3041
  "task.act.unblock": taskAct("unblock"),
2794
3042
  "task.act.repair-dependency": taskAct("repair-dependency"),
2795
3043
  "task.act.follow-up": taskAct("follow-up"),
3044
+ "run.act.reveal": runReveal,
2796
3045
  "run.act.note": runNote,
2797
3046
  "run.act.comment": runComment,
2798
3047
  "run.act.revise": runRevise,