toolroll 0.9.53 → 0.9.55

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 (254) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-ask.js +8 -8
  19. package/dist/chat-core.d.ts +171 -0
  20. package/dist/chat-core.js +288 -0
  21. package/dist/chat-decide.d.ts +10 -8
  22. package/dist/chat-decide.js +26 -21
  23. package/dist/chat-delivery-state.d.ts +51 -75
  24. package/dist/chat-delivery-state.js +140 -170
  25. package/dist/chat-delivery.d.ts +21 -2
  26. package/dist/chat-delivery.js +313 -194
  27. package/dist/chat-flow.d.ts +29 -1
  28. package/dist/chat-flow.js +24 -50
  29. package/dist/chat-messages.d.ts +319 -0
  30. package/dist/chat-messages.js +841 -0
  31. package/dist/chat-migration.d.ts +30 -0
  32. package/dist/chat-migration.js +802 -0
  33. package/dist/chat-polish.d.ts +5 -4
  34. package/dist/chat-polish.js +9 -10
  35. package/dist/chat-quiet.d.ts +3 -0
  36. package/dist/chat-quiet.js +5 -0
  37. package/dist/check-levels-ui.d.ts +2 -1
  38. package/dist/check-levels-ui.js +26 -53
  39. package/dist/cli.d.ts +3 -1
  40. package/dist/cli.js +9 -3
  41. package/dist/coding-shipping-ui.d.ts +2 -1
  42. package/dist/coding-shipping-ui.js +6 -6
  43. package/dist/coding-ui.d.ts +3 -1
  44. package/dist/coding-ui.js +30 -29
  45. package/dist/contracts/chat-content.d.ts +71 -2
  46. package/dist/contracts/chat-content.js +22 -3
  47. package/dist/contracts/chat-tables.d.ts +11 -4
  48. package/dist/contracts/chat-tables.js +165 -87
  49. package/dist/contracts/cli.d.ts +1 -3
  50. package/dist/contracts/console-api.d.ts +1 -1
  51. package/dist/contracts/console-api.js +6 -5
  52. package/dist/contracts/discord-callback.js +2 -1
  53. package/dist/contracts/health.d.ts +1 -3
  54. package/dist/contracts/slack-callback.js +3 -2
  55. package/dist/contracts/store-json.d.ts +1 -5
  56. package/dist/contracts/store-json.js +1 -4
  57. package/dist/contracts/teams-callback.js +2 -1
  58. package/dist/control-ui.d.ts +5 -4
  59. package/dist/control-ui.js +9 -34
  60. package/dist/credentials-ui.d.ts +5 -4
  61. package/dist/credentials-ui.js +23 -24
  62. package/dist/demo-chat.d.ts +3 -3
  63. package/dist/demo-chat.js +29 -76
  64. package/dist/discord-chat.js +19 -38
  65. package/dist/discord-settings.d.ts +3 -2
  66. package/dist/discord-settings.js +21 -55
  67. package/dist/discord.js +4 -14
  68. package/dist/envelope.js +10 -0
  69. package/dist/evidence-pack.d.ts +2 -1
  70. package/dist/evidence-pack.js +45 -49
  71. package/dist/export-ui.d.ts +7 -2
  72. package/dist/export-ui.js +4 -10
  73. package/dist/export.js +4 -4
  74. package/dist/flow-gallery-ui.d.ts +4 -3
  75. package/dist/flow-gallery-ui.js +28 -43
  76. package/dist/flow-starters-ui.d.ts +2 -1
  77. package/dist/flow-starters-ui.js +12 -15
  78. package/dist/flow-triggers.d.ts +1 -0
  79. package/dist/flow-triggers.js +16 -14
  80. package/dist/flows-ui.d.ts +4 -3
  81. package/dist/flows-ui.js +19 -30
  82. package/dist/flows.js +2 -1
  83. package/dist/guides.js +108 -3
  84. package/dist/html.d.ts +76 -0
  85. package/dist/html.js +173 -0
  86. package/dist/integrations-ui.d.ts +2 -1
  87. package/dist/integrations-ui.js +19 -23
  88. package/dist/kits-ui.d.ts +5 -4
  89. package/dist/kits-ui.js +18 -20
  90. package/dist/knowledge-ui.d.ts +6 -5
  91. package/dist/knowledge-ui.js +26 -25
  92. package/dist/lead-context.d.ts +11 -1
  93. package/dist/lead-context.js +6 -8
  94. package/dist/ledger-view.d.ts +2 -1
  95. package/dist/ledger-view.js +27 -29
  96. package/dist/limits-ui.d.ts +2 -1
  97. package/dist/limits-ui.js +7 -7
  98. package/dist/live-bus.d.ts +50 -8
  99. package/dist/live-bus.js +133 -50
  100. package/dist/mate-tools.js +3 -2
  101. package/dist/mcp-oauth.js +35 -36
  102. package/dist/models-ui.d.ts +2 -1
  103. package/dist/models-ui.js +25 -35
  104. package/dist/modes.d.ts +13 -1
  105. package/dist/modes.js +17 -1
  106. package/dist/monitoring-ui.d.ts +4 -3
  107. package/dist/monitoring-ui.js +13 -26
  108. package/dist/notification-rows.d.ts +30 -0
  109. package/dist/notification-rows.js +102 -0
  110. package/dist/openrouter-models.d.ts +2 -1
  111. package/dist/openrouter-models.js +5 -9
  112. package/dist/operate.js +16 -9
  113. package/dist/people-audit-ui.d.ts +2 -1
  114. package/dist/people-audit-ui.js +13 -13
  115. package/dist/plane-review.js +5 -11
  116. package/dist/policy-ui.d.ts +3 -2
  117. package/dist/policy-ui.js +17 -23
  118. package/dist/project-delete-ui.d.ts +4 -3
  119. package/dist/project-delete-ui.js +15 -27
  120. package/dist/project-delete.js +21 -26
  121. package/dist/pull-request-ui.d.ts +2 -1
  122. package/dist/pull-request-ui.js +17 -32
  123. package/dist/recipe-ui.d.ts +8 -7
  124. package/dist/recipe-ui.js +39 -32
  125. package/dist/release-adapters.d.ts +60 -0
  126. package/dist/release-adapters.js +435 -0
  127. package/dist/release-cli.d.ts +24 -0
  128. package/dist/release-cli.js +181 -0
  129. package/dist/release-gate.d.ts +80 -0
  130. package/dist/release-gate.js +150 -0
  131. package/dist/release-links.d.ts +29 -0
  132. package/dist/release-links.js +102 -0
  133. package/dist/release.d.ts +331 -0
  134. package/dist/release.js +550 -0
  135. package/dist/reply-shape.d.ts +2 -1
  136. package/dist/reply-shape.js +5 -5
  137. package/dist/repository-context-ui.d.ts +2 -1
  138. package/dist/repository-context-ui.js +2 -7
  139. package/dist/request-budget-ui.d.ts +2 -1
  140. package/dist/request-budget-ui.js +16 -16
  141. package/dist/result-review.d.ts +2 -3
  142. package/dist/result-review.js +3 -9
  143. package/dist/result-shots.js +2 -2
  144. package/dist/retention-ui.d.ts +7 -1
  145. package/dist/retention-ui.js +7 -11
  146. package/dist/retention.js +1 -1
  147. package/dist/serve.d.ts +11 -2
  148. package/dist/serve.js +69 -58
  149. package/dist/server/chat.d.ts +3 -6
  150. package/dist/server/chat.js +969 -1006
  151. package/dist/server/chrome.d.ts +291 -0
  152. package/dist/server/chrome.js +747 -0
  153. package/dist/server/console.css +2819 -0
  154. package/dist/server/flows.d.ts +2 -6
  155. package/dist/server/flows.js +840 -766
  156. package/dist/server/guards.d.ts +1 -1
  157. package/dist/server/guards.js +5 -2
  158. package/dist/server/handler-context.d.ts +1 -1
  159. package/dist/server/handler-context.js +1 -1
  160. package/dist/server/handler-registry.d.ts +8 -6
  161. package/dist/server/handler-registry.js +14 -2
  162. package/dist/server/http.d.ts +274 -0
  163. package/dist/server/http.js +239 -0
  164. package/dist/server/live.d.ts +34 -0
  165. package/dist/server/live.js +309 -0
  166. package/dist/server/pages.d.ts +2 -6
  167. package/dist/server/pages.js +808 -789
  168. package/dist/server/people-tokens.d.ts +1 -6
  169. package/dist/server/people-tokens.js +580 -572
  170. package/dist/server/remote.d.ts +4 -7
  171. package/dist/server/remote.js +37 -70
  172. package/dist/server/render-chat.d.ts +402 -0
  173. package/dist/server/render-chat.js +927 -0
  174. package/dist/server/render-pages.d.ts +535 -0
  175. package/dist/server/render-pages.js +1277 -0
  176. package/dist/server/render-people.d.ts +32 -0
  177. package/dist/server/render-people.js +121 -0
  178. package/dist/server/render-results.d.ts +744 -0
  179. package/dist/server/render-results.js +2237 -0
  180. package/dist/server/render-settings.d.ts +147 -0
  181. package/dist/server/render-settings.js +577 -0
  182. package/dist/server/render-tasks.d.ts +632 -0
  183. package/dist/server/render-tasks.js +2345 -0
  184. package/dist/server/request-context.d.ts +46 -0
  185. package/dist/server/request-context.js +17 -0
  186. package/dist/server/route-table.d.ts +25 -3
  187. package/dist/server/route-table.js +163 -75
  188. package/dist/server/runtime.d.ts +15 -7
  189. package/dist/server/runtime.js +7 -1
  190. package/dist/server/session.d.ts +138 -0
  191. package/dist/server/session.js +94 -0
  192. package/dist/server/settings.d.ts +1 -5
  193. package/dist/server/settings.js +2182 -2117
  194. package/dist/server/task-views.d.ts +11 -4
  195. package/dist/server/task-views.js +26 -22
  196. package/dist/server/tasks.d.ts +9 -9
  197. package/dist/server/tasks.js +1486 -1418
  198. package/dist/server-telemetry.d.ts +4 -3
  199. package/dist/server-telemetry.js +4 -8
  200. package/dist/skills-ui.d.ts +4 -3
  201. package/dist/skills-ui.js +14 -17
  202. package/dist/skills.d.ts +1 -1
  203. package/dist/skills.js +2 -1
  204. package/dist/slack-api.d.ts +5 -1
  205. package/dist/slack-api.js +2 -2
  206. package/dist/slack-chat.d.ts +9 -4
  207. package/dist/slack-chat.js +29 -52
  208. package/dist/slack-settings.d.ts +3 -2
  209. package/dist/slack-settings.js +23 -57
  210. package/dist/slack.js +6 -16
  211. package/dist/spend-ui.d.ts +2 -1
  212. package/dist/spend-ui.js +9 -32
  213. package/dist/sso-ui.d.ts +3 -2
  214. package/dist/sso-ui.js +26 -27
  215. package/dist/storage-ui.d.ts +2 -1
  216. package/dist/storage-ui.js +14 -33
  217. package/dist/store.d.ts +97 -364
  218. package/dist/store.js +207 -1531
  219. package/dist/surface.js +14 -3
  220. package/dist/task-composer.d.ts +2 -1
  221. package/dist/task-composer.js +7 -7
  222. package/dist/task-status.d.ts +4 -3
  223. package/dist/task-status.js +15 -16
  224. package/dist/team-http.d.ts +5 -9
  225. package/dist/team-http.js +5 -63
  226. package/dist/team-ui.d.ts +2 -1
  227. package/dist/team-ui.js +2 -5
  228. package/dist/teammate-question.js +12 -12
  229. package/dist/teammates-ui.d.ts +5 -4
  230. package/dist/teammates-ui.js +61 -90
  231. package/dist/teams-chat.js +12 -9
  232. package/dist/teams-settings.d.ts +3 -2
  233. package/dist/teams-settings.js +22 -33
  234. package/dist/teams.js +4 -5
  235. package/dist/telegram-flow.js +5 -5
  236. package/dist/telegram-settings.d.ts +3 -2
  237. package/dist/telegram-settings.js +14 -25
  238. package/dist/telegram.js +4 -4
  239. package/dist/toolroll-update-ui.d.ts +3 -2
  240. package/dist/toolroll-update-ui.js +35 -39
  241. package/dist/toolroll-update.d.ts +10 -0
  242. package/dist/toolroll-update.js +50 -3
  243. package/dist/tools-ui.d.ts +5 -2
  244. package/dist/tools-ui.js +31 -31
  245. package/dist/when-html.d.ts +11 -3
  246. package/dist/when-html.js +5 -6
  247. package/dist/workspace-revision.js +3 -1
  248. package/dist/workspace-ui.d.ts +4 -2
  249. package/dist/workspace-ui.js +10 -10
  250. package/package.json +1 -1
  251. package/dist/server/shared.d.ts +0 -3151
  252. package/dist/server/shared.js +0 -12001
  253. package/dist/slack-state.d.ts +0 -13
  254. package/dist/slack-state.js +0 -10
@@ -1,4 +1,6 @@
1
+ import { handlersOf } from './handler-registry.js';
1
2
  /** tasks handlers, moved without changing their route bodies. */
3
+ import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
2
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
3
5
  import { lstatSync, realpathSync } from "node:fs";
4
6
  import {} from "node:http";
@@ -44,1395 +46,1442 @@ import { templateByName } from "../templates.js";
44
46
  import { TEXT_LIMITS } from "../text-limits.js";
45
47
  import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
46
48
  import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
47
- import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest, nonceHashOf, approveRefusalWords, boardBody, chatReturnWithSaid, checkProgressHtml, consentDoorOf, decisionPage, donePage, editorFileHref, escape, inboxFingerprints, inboxPage, matchTaskPath, newTaskPage, nextPage, oneLineOf, parseInboxTab, PermissionsWouldChange, proofBundleView, QUEUE_FRONT, QUEUE_VIEW, queueBody, queueScript, rankReviewQueue, redirect, refuse, regionScript, requestContext, requirementsFromEditor, respond, RESULT_REFUSALS, resumeDigestOf, REVIEW_QUEUE_CAP, reviewCockpitPage, reviewHref, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, safeChatReturn, safeReturn, SAFETY, screen, TASK_STATES, taskChatHref, taskHref, taskOf, tasksPage, transcriptScript, whenTime, withRefusal, workPage } from "./shared.js";
49
+ import { requestContext } from "./request-context.js";
50
+ import { oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
51
+ import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
52
+ import { boardBody, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, tasksPage, workPage } from "./render-pages.js";
53
+ 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";
54
+ import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
55
+ import {} from "./session.js";
48
56
  export function createTasksHandlers(runtime) {
49
57
  const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, matePrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
50
- async function get(ctx) {
51
- const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
52
- if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
53
- if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
54
- return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
55
- const actor = { name: who.name, generation: who.session.generation };
56
- const match = /^\/code\/([a-f0-9]{32})(?:\/(state|changes|ship))?$/.exec(url.pathname);
57
- try {
58
- const selected = match && runtime.coding ? runtime.coding.get(match[1], actor) : null;
59
- if (selected && !codingProjectAllowed(selected.repo))
60
- return refuse(response, who, 403, 'That project is outside your access.', '/projects');
61
- if (match?.[2] === 'ship' && selected) {
62
- const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
63
- return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf), { chrome: chromeFor(selected.repo, 'code') }));
64
- }
65
- if (match?.[2]) {
66
- if (!selected || !runtime.coding)
67
- throw Error('The coding session is unavailable.');
68
- if (match[2] === 'state' && url.searchParams.get('revision') === String(runtime.coding.revision(selected.id, actor)))
69
- return respond(response, 200, 'application/json', JSON.stringify({ unchanged: true }));
70
- const result = match[2] === 'changes' ? await runtime.coding.changes(selected.id, actor) : runtime.coding.snapshot(selected.id, actor);
71
- if (!codingActorAllowed(actor) || !codingProjectAllowed(selected.repo))
72
- return respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }));
73
- return respond(response, 200, 'application/json', JSON.stringify(result));
74
- }
75
- if (url.pathname !== '/code' && !match)
76
- return refuse(response, who, 404, 'Coding session not found.', '/code');
77
- const requestedProject = url.searchParams.get('project');
78
- if (requestedProject !== null && (!visible(requestedProject) || ![...managedRepos(), ...store.listProjects().map(p => p.path)].includes(requestedProject)))
79
- return refuse(response, who, 403, 'That project is outside your access.', '/projects');
80
- const codeProject = selected?.repo ?? requestedProject ?? project;
81
- const chrome = chromeFor(codeProject, 'code');
82
- const content = codingWorkspaceHtml({ owner: who.name, projects: chrome.projects ?? [], sessions: runtime.coding?.list(actor).filter(s => codingProjectAllowed(s.repo)) ?? [], selected: selected && runtime.coding ? runtime.coding.snapshot(selected.id, actor) : null, csrf: who.session.csrf, project: codeProject, available: runtime.coding !== null, ...(runtime.codingProblem ? { error: runtime.codingProblem } : {}) });
83
- return sendScreen(response, 200, screen('Code', content, { chrome, functional: { script: codingWorkspaceScript(), fetches: true } }));
84
- }
85
- catch (error) {
86
- if (!codingActorAllowed(actor))
87
- return match?.[2] && match[2] !== 'ship'
88
- ? respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }))
89
- : refuse(response, who, 403, 'Your access changed. Sign in again.', '/work');
90
- const message = error instanceof Error ? error.message : 'The coding workspace is unavailable.';
91
- if (match?.[2] === 'ship')
92
- return sendScreen(response, 409, screen('Review for shipping', `<h1>Prepare this result</h1><p>${escape(message)}</p><a href="/code/${match[1]}">Back to coding</a>`, { chrome: chromeFor(project, 'code') }));
93
- if (match?.[2])
94
- return respond(response, 409, 'application/json', JSON.stringify({ ok: false, error: message }));
95
- return sendScreen(response, 404, screen('Coding session unavailable', `<h1>Session unavailable</h1><p>${escape(message)}</p><a href="/code">Open coding workspace</a>`, { chrome: chromeFor(project, 'code') }));
96
- }
58
+ /** A row admitted the address but its path parameter does not parse: the console's unknown page. */
59
+ function noPage(who, response) {
60
+ return refuse(response, who, 404, "There's no page at this address.", "/chat");
61
+ }
62
+ const showCode = (part) => async (ctx) => {
63
+ const { url, who, response, project } = ctx;
64
+ if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
65
+ return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
66
+ const actor = { name: who.name, generation: who.session.generation };
67
+ const sessionId = part === "workspace" || part === "unknown" ? null : /^\/code\/([a-f0-9]{32})/.exec(url.pathname)[1];
68
+ const read = part === "state" || part === "changes" ? part : null;
69
+ try {
70
+ const selected = sessionId !== null && runtime.coding ? runtime.coding.get(sessionId, actor) : null;
71
+ if (selected && !codingProjectAllowed(selected.repo))
72
+ return refuse(response, who, 403, 'That project is outside your access.', '/projects');
73
+ if (part === 'ship' && selected) {
74
+ const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
75
+ return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview), { chrome: chromeFor(selected.repo, 'code') }));
76
+ }
77
+ if (part === 'ship' || read !== null) {
78
+ if (!selected || !runtime.coding)
79
+ throw Error('The coding session is unavailable.');
80
+ if (read === 'state' && url.searchParams.get('revision') === String(runtime.coding.revision(selected.id, actor)))
81
+ return respond(response, 200, 'application/json', JSON.stringify({ unchanged: true }));
82
+ const result = read === 'changes' ? await runtime.coding.changes(selected.id, actor) : runtime.coding.snapshot(selected.id, actor);
83
+ if (!codingActorAllowed(actor) || !codingProjectAllowed(selected.repo))
84
+ return respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }));
85
+ return respond(response, 200, 'application/json', JSON.stringify(result));
86
+ }
87
+ if (part === "unknown")
88
+ return refuse(response, who, 404, 'Coding session not found.', '/code');
89
+ const requestedProject = url.searchParams.get('project');
90
+ if (requestedProject !== null && (!visible(requestedProject) || ![...managedRepos(), ...store.listProjects().map(p => p.path)].includes(requestedProject)))
91
+ return refuse(response, who, 403, 'That project is outside your access.', '/projects');
92
+ const codeProject = selected?.repo ?? requestedProject ?? project;
93
+ const chrome = chromeFor(codeProject, 'code');
94
+ const content = codingWorkspaceHtml({ owner: who.name, projects: chrome.projects ?? [], sessions: runtime.coding?.list(actor).filter(s => codingProjectAllowed(s.repo)) ?? [], selected: selected && runtime.coding ? runtime.coding.snapshot(selected.id, actor) : null, csrf: who.session.csrf, project: codeProject, available: runtime.coding !== null, ...(runtime.codingProblem ? { error: runtime.codingProblem } : {}) });
95
+ return sendScreen(response, 200, screen('Code', content, { chrome, functional: { script: codingWorkspaceScript(), fetches: true } }));
97
96
  }
98
- if (url.pathname === "/inbox") {
99
- // With no project open in scoped mode this is the ROLL-UP inbox:
100
- // admission binds inside the bounded queries, every row's repo is
101
- // re-proved here, and rows render as links only (Codex roll-up
102
- // review, findings 6–8). Gaps stay per-project — they are derived
103
- // against one repo's capabilities and roll up dishonestly.
104
- const rollup = project === null && !unscopedMode;
105
- const admission = rollup ? admissionList() : null;
106
- const cancelled = store
107
- .listCancelledBlockersScoped(project, 10, admission)
108
- .filter(one => visible(one.repo) && visible(one.blockerRepo));
109
- const inboxData = {
110
- csrf: who.via === "cookie" ? who.session.csrf : "",
111
- revision: who.via === "cookie" ? who.session.projectRevision : 0,
112
- rollup,
113
- interactive: project !== null,
114
- decisions: store.listDecisionsScoped(project).filter(one => visible(one.repo)).slice(0, 10),
115
- // Each waiting scope wears its consent door (v48 authority repair): a scope whose
116
- // approval is closed — unreadable route, a route that cannot run,
117
- // a pre-routing row whose approval lapsed — reads as needing
118
- // attention, never as something to approve.
119
- approvals: store.scopesAwaitingApproval(project, 10, admission).filter(one => visible(one.repo)).map(one => {
120
- const ref = store.lookupRef(one.taskId);
121
- const scope = store.getScope(one.taskId);
122
- const door = consentDoorOf(scope, routeViewOf(one.taskId, ref, scope, now, who));
123
- return { ...one, closed: door.open ? null : door.title };
124
- }),
125
- requeueables: store.listRequeueablesScoped(project, now, 10, admission).filter(one => visible(one.repo)),
126
- needsVerification: store
127
- .listCompletedWorkScoped(project, 10, admission)
128
- .filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
129
- assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
130
- .map(one => ({
131
- taskId: one.taskId,
132
- title: one.title,
133
- verdict: one.proofVerdict,
134
- repo: one.repo,
135
- matrix: one.proofMatrix,
136
- repairChain: one.runId === null ? null : store.repairChainFor(one.runId),
137
- })),
138
- cancelledBlockers: cancelled,
139
- gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
140
- wizard: firstRunStepsNow(now),
141
- worker: (() => {
142
- // The one fact the inbox must never hide (install review): with
143
- // no worker answering, nothing here will ever build, and every
144
- // approval below is a promise nobody is there to keep.
145
- const runners = store.listRunners().filter(one => one.retiredAt === null && (!restricted() || one.repos.some(visible)));
146
- const answering = runners.filter(one => runnerAlive(one, now));
147
- const lastHeard = runners.map(one => one.heartbeatAt).sort().at(-1) ?? null;
148
- return { answering: answering.length, registered: runners.length, lastHeard };
149
- })(),
150
- now,
151
- // Console v2: results ready to review and work running now, from the same admitted work index.
152
- ...(() => {
153
- const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
154
- const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
155
- const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
156
- return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
157
- })(),
158
- };
159
- // Which tabs hold something new since this browser last looked (the dots on a phone).
160
- const tab = parseInboxTab(url.searchParams.get("tab")) ?? "all";
161
- const prints = inboxFingerprints(inboxData);
162
- const seenWords = /(?:^|;\s*)so-inbox-seen=([0-9a-f]{8}(?:\.[0-9a-f]{8}){3})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1]?.split(".") ?? null;
163
- const order = ["needs-you", "ready", "running", "all"];
164
- const seen = Object.fromEntries(order.map((one, index) => [one, seenWords?.[index] ?? ""]));
165
- const unread = order.filter(one => seenWords !== null && seen[one] !== prints[one]);
166
- for (const one of order)
167
- if (tab === "all" || one === tab)
168
- seen[one] = prints[one];
169
- response.setHeader("Set-Cookie", `so-inbox-seen=${order.map(one => seen[one] || prints[one]).join(".")}; SameSite=Lax; Path=/; Max-Age=31536000`);
170
- return sendScreen(response, 200, inboxPage(chromeFor(project, "inbox"), { ...inboxData, tab, unread }));
171
- }
172
- if (url.pathname === "/work") {
173
- // The Work destination (workspace package 1): every task in view as
174
- // one row wearing the shared status projection, with All, Needs you,
175
- // Running, and Completed as shortcuts over the same rows — never a
176
- // persisted state. With no project open in scoped mode this rolls
177
- // up like the inbox: admission binds the query and every row's repo
178
- // is re-proved here.
179
- const view = parseWorkView(url.searchParams.get("view"));
180
- const rollup = project === null && !unscopedMode;
181
- let work;
182
- try {
183
- work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
184
- const facts = requestContext.getStore();
185
- if (facts !== undefined) {
186
- // These counts cover the exact admitted project lens, including
187
- // unplaced tasks where permitted. Crew excludes unplaced rows.
188
- if (project === null)
189
- facts.workCounts = work.projects;
190
- const admitted = workAccess().repos;
191
- const crewLens = project !== null || admitted !== null && JSON.stringify([...admitted].sort()) === JSON.stringify(managedRepos().sort()) && !work.projects.some(one => one.repo === null);
192
- if (view === 'all' && !url.searchParams.has('cursor') && crewLens)
193
- facts.workCrew = { project, page: work };
194
- }
195
- }
196
- catch (error) {
197
- if (error instanceof WorkIndexCursorError)
198
- return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
199
- throw error;
97
+ catch (error) {
98
+ if (!codingActorAllowed(actor))
99
+ return read !== null
100
+ ? respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }))
101
+ : refuse(response, who, 403, 'Your access changed. Sign in again.', '/work');
102
+ const message = error instanceof Error ? error.message : 'The coding workspace is unavailable.';
103
+ if (part === 'ship')
104
+ return sendScreen(response, 409, screen('Review for shipping', html `<h1>Prepare this result</h1><p>${message}</p><a href="/code/${sessionId}">Back to coding</a>`, { chrome: chromeFor(project, 'code') }));
105
+ if (read !== null)
106
+ return respond(response, 409, 'application/json', JSON.stringify({ ok: false, error: message }));
107
+ return sendScreen(response, 404, screen('Coding session unavailable', html `<h1>Session unavailable</h1><p>${message}</p><a href="/code">Open coding workspace</a>`, { chrome: chromeFor(project, 'code') }));
108
+ }
109
+ };
110
+ async function showInbox(ctx) {
111
+ const { url, who, request, response, now, project } = ctx;
112
+ // With no project open in scoped mode this is the ROLL-UP inbox:
113
+ // admission binds inside the bounded queries, every row's repo is
114
+ // re-proved here, and rows render as links only (Codex roll-up
115
+ // review, findings 6–8). Gaps stay per-project — they are derived
116
+ // against one repo's capabilities and roll up dishonestly.
117
+ const rollup = project === null && !unscopedMode;
118
+ const admission = rollup ? admissionList() : null;
119
+ const cancelled = store
120
+ .listCancelledBlockersScoped(project, 10, admission)
121
+ .filter(one => visible(one.repo) && visible(one.blockerRepo));
122
+ const inboxData = {
123
+ csrf: who.via === "cookie" ? who.session.csrf : "",
124
+ revision: who.via === "cookie" ? who.session.projectRevision : 0,
125
+ rollup,
126
+ interactive: project !== null,
127
+ decisions: store.listDecisionsScoped(project).filter(one => visible(one.repo)).slice(0, 10),
128
+ // Each waiting scope wears its consent door (v48 authority repair): a scope whose
129
+ // approval is closed — unreadable route, a route that cannot run,
130
+ // a pre-routing row whose approval lapsed — reads as needing
131
+ // attention, never as something to approve.
132
+ approvals: store.scopesAwaitingApproval(project, 10, admission).filter(one => visible(one.repo)).map(one => {
133
+ const ref = store.lookupRef(one.taskId);
134
+ const scope = store.getScope(one.taskId);
135
+ const door = consentDoorOf(scope, routeViewOf(one.taskId, ref, scope, now, who));
136
+ return { ...one, closed: door.open ? null : door.title };
137
+ }),
138
+ requeueables: store.listRequeueablesScoped(project, now, 10, admission).filter(one => visible(one.repo)),
139
+ needsVerification: store
140
+ .listCompletedWorkScoped(project, 10, admission)
141
+ .filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
142
+ assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
143
+ .map(one => ({
144
+ taskId: one.taskId,
145
+ title: one.title,
146
+ verdict: one.proofVerdict,
147
+ repo: one.repo,
148
+ matrix: one.proofMatrix,
149
+ repairChain: one.runId === null ? null : store.repairChainFor(one.runId),
150
+ })),
151
+ cancelledBlockers: cancelled,
152
+ gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
153
+ wizard: firstRunStepsNow(now),
154
+ worker: (() => {
155
+ // The one fact the inbox must never hide (install review): with
156
+ // no worker answering, nothing here will ever build, and every
157
+ // approval below is a promise nobody is there to keep.
158
+ const runners = store.listRunners().filter(one => one.retiredAt === null && (!restricted() || one.repos.some(visible)));
159
+ const answering = runners.filter(one => runnerAlive(one, now));
160
+ const lastHeard = runners.map(one => one.heartbeatAt).sort().at(-1) ?? null;
161
+ return { answering: answering.length, registered: runners.length, lastHeard };
162
+ })(),
163
+ now,
164
+ // Console v2: results ready to review and work running now, from the same admitted work index.
165
+ ...(() => {
166
+ const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
167
+ const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
168
+ const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
169
+ return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
170
+ })(),
171
+ };
172
+ // Which tabs hold something new since this browser last looked (the dots on a phone).
173
+ const tab = parseInboxTab(url.searchParams.get("tab")) ?? "all";
174
+ const prints = inboxFingerprints(inboxData);
175
+ const seenWords = /(?:^|;\s*)so-inbox-seen=([0-9a-f]{8}(?:\.[0-9a-f]{8}){3})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1]?.split(".") ?? null;
176
+ const order = ["needs-you", "ready", "running", "all"];
177
+ const seen = Object.fromEntries(order.map((one, index) => [one, seenWords?.[index] ?? ""]));
178
+ const unread = order.filter(one => seenWords !== null && seen[one] !== prints[one]);
179
+ for (const one of order)
180
+ if (tab === "all" || one === tab)
181
+ seen[one] = prints[one];
182
+ response.setHeader("Set-Cookie", `so-inbox-seen=${order.map(one => seen[one] || prints[one]).join(".")}; SameSite=Lax; Path=/; Max-Age=31536000`);
183
+ return sendScreen(response, 200, inboxPage(chromeFor(project, "inbox"), { ...inboxData, tab, unread }));
184
+ }
185
+ async function showWork(ctx) {
186
+ const { url, who, response, now, project } = ctx;
187
+ // The Work destination (workspace package 1): every task in view as
188
+ // one row wearing the shared status projection, with All, Needs you,
189
+ // Running, and Completed as shortcuts over the same rows — never a
190
+ // persisted state. With no project open in scoped mode this rolls
191
+ // up like the inbox: admission binds the query and every row's repo
192
+ // is re-proved here.
193
+ const view = parseWorkView(url.searchParams.get("view"));
194
+ const rollup = project === null && !unscopedMode;
195
+ let work;
196
+ try {
197
+ work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
198
+ const facts = requestContext.getStore();
199
+ if (facts !== undefined) {
200
+ // These counts cover the exact admitted project lens, including
201
+ // unplaced tasks where permitted. Crew excludes unplaced rows.
202
+ if (project === null)
203
+ facts.workCounts = work.projects;
204
+ const admitted = workAccess().repos;
205
+ const crewLens = project !== null || admitted !== null && JSON.stringify([...admitted].sort()) === JSON.stringify(managedRepos().sort()) && !work.projects.some(one => one.repo === null);
206
+ if (view === 'all' && !url.searchParams.has('cursor') && crewLens)
207
+ facts.workCrew = { project, page: work };
200
208
  }
201
- // A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
202
- // the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
203
- // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
204
- // current plan (one saved file, for live rows only), and without it the row names the step by number.
205
- work = { ...work, items: work.items.map((item) => {
206
- if (item.liveRunId !== null) {
207
- const ref = store.lookupRef(item.activeTaskId);
208
- const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
209
- if (ref === null || recorded === null || recorded.taskRef !== ref.id)
210
- return item;
211
- const plan = store.currentPlanRevision(ref.id);
212
- const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
213
- const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
214
- const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
215
- return steps === null ? item : { ...item, progress: steps.line };
216
- }
217
- if (item.state !== "failed")
218
- return item;
219
- const family = familyOf(item.activeTaskId);
220
- if (family === null || family.problem !== null)
209
+ }
210
+ catch (error) {
211
+ if (error instanceof WorkIndexCursorError)
212
+ return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
213
+ throw error;
214
+ }
215
+ // A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
216
+ // the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
217
+ // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
218
+ // current plan (one saved file, for live rows only), and without it the row names the step by number.
219
+ work = { ...work, items: work.items.map((item) => {
220
+ if (item.liveRunId !== null) {
221
+ const ref = store.lookupRef(item.activeTaskId);
222
+ const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
223
+ if (ref === null || recorded === null || recorded.taskRef !== ref.id)
221
224
  return item;
222
- const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
223
- return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
224
- }) };
225
- const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
226
- view,
227
- ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
228
- work,
229
- previous: url.searchParams.has('cursor'),
230
- multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
231
- now,
232
- // v105: subscription windows and monthly budgets, for whoever runs the installation.
233
- limits: store.isInstanceOperator(who.name)
234
- ? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, teammate: id => teammateNamesOf(store.handle).get(id) ?? `Teammate ${id}` }, now)
235
- : null,
236
- });
237
- // One project's Tasks dock that project's own conversation (v77).
238
- const projectThread = url.searchParams.has('project') && project !== null && managedRepos().includes(project) ? project : null;
239
- const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
240
- if (docked !== null)
241
- page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
242
- // Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
243
- page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
244
- return sendScreen(response, 200, page);
245
- }
246
- if (url.pathname === "/next") {
247
- // Triage: everything waiting on a person, one at a time, hardest-
248
- // blocked first — oldest question, then plans and scopes to approve,
249
- // then stalled work to retry, then requirement gaps. `skip` is a
250
- // bounded, session-free cursor of keys the operator set aside; every
251
- // act 303s back here, which is what makes it a flow and not a list.
252
- const skipped = new Set((url.searchParams.get("skip") ?? "").split(",").filter(one => /^[darg]:[A-Za-z0-9._:-]{1,80}$/.test(one)).slice(0, 20));
253
- const decisions = store.listDecisionsScoped(project).filter(one => one.state !== "answered");
254
- const approvals = store.scopesAwaitingApproval(project, 20);
255
- const requeueables = store.listRequeueablesScoped(project, now, 20);
256
- const gaps = project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0);
257
- const queue = [
258
- ...decisions.map(decision => ({ key: `d:${decision.id}`, kind: "decision", decision })),
259
- ...approvals.map(approval => ({ key: `a:${approval.taskId}`, kind: "approval", approval })),
260
- ...requeueables.map(stalled => ({ key: `r:${stalled.taskId}`, kind: "requeue", stalled })),
261
- ...gaps.map(gap => ({ key: `g:${gap.key.replace(/[^A-Za-z0-9._:-]/g, "_")}`, kind: "gap", gap })),
262
- ];
263
- const remaining = queue.filter(one => !skipped.has(one.key));
264
- const item = remaining[0] ?? null;
265
- const csrf = who.via === "cookie" ? who.session.csrf : "";
266
- if (item !== null && item.kind === "approval") {
267
- // The card restates the digest-bound terms, so the nonce may be
268
- // minted here — same rule as the task screen, same binding.
269
- const ref = store.lookupRef(item.approval.taskId);
270
- const scope = store.getScope(item.approval.taskId);
271
- const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
272
- const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
273
- // The same agents block the task page and chat sign under (v48),
274
- // and the same consent door: closed, it mints no nonce.
275
- const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
276
- const nonce = who.via === "cookie" && consentDoorOf(scope, route).open ? mintApprovalNonce(who.name, item.approval.taskId, approvalDigest) : "";
277
- return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
278
- item, scope,
279
- planDocument: planView?.document ?? null,
280
- planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
281
- approvalDigest,
282
- deliverable: ref?.deliverable ?? "branch",
283
- route,
284
- csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
285
- }));
286
- }
225
+ const plan = store.currentPlanRevision(ref.id);
226
+ const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
227
+ const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
228
+ const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
229
+ return steps === null ? item : { ...item, progress: steps.line };
230
+ }
231
+ if (item.state !== "failed")
232
+ return item;
233
+ const family = familyOf(item.activeTaskId);
234
+ if (family === null || family.problem !== null)
235
+ return item;
236
+ const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
237
+ return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
238
+ }) };
239
+ const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
240
+ view,
241
+ ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
242
+ work,
243
+ previous: url.searchParams.has('cursor'),
244
+ multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
245
+ now,
246
+ // v105: subscription windows and monthly budgets, for whoever runs the installation.
247
+ limits: store.isInstanceOperator(who.name)
248
+ ? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, teammate: id => teammateNamesOf(store.handle).get(id) ?? `Teammate ${id}` }, now)
249
+ : null,
250
+ });
251
+ // One project's Tasks dock that project's own conversation (v77).
252
+ const projectThread = url.searchParams.has('project') && project !== null && managedRepos().includes(project) ? project : null;
253
+ const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
254
+ if (docked !== null)
255
+ page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
256
+ // Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
257
+ page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
258
+ return sendScreen(response, 200, page);
259
+ }
260
+ async function showNext(ctx) {
261
+ const { url, who, response, now, project } = ctx;
262
+ // Triage: everything waiting on a person, one at a time, hardest-
263
+ // blocked first — oldest question, then plans and scopes to approve,
264
+ // then stalled work to retry, then requirement gaps. `skip` is a
265
+ // bounded, session-free cursor of keys the operator set aside; every
266
+ // act 303s back here, which is what makes it a flow and not a list.
267
+ const skipped = new Set((url.searchParams.get("skip") ?? "").split(",").filter(one => /^[darg]:[A-Za-z0-9._:-]{1,80}$/.test(one)).slice(0, 20));
268
+ const decisions = store.listDecisionsScoped(project).filter(one => one.state !== "answered");
269
+ const approvals = store.scopesAwaitingApproval(project, 20);
270
+ const requeueables = store.listRequeueablesScoped(project, now, 20);
271
+ const gaps = project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0);
272
+ const queue = [
273
+ ...decisions.map(decision => ({ key: `d:${decision.id}`, kind: "decision", decision })),
274
+ ...approvals.map(approval => ({ key: `a:${approval.taskId}`, kind: "approval", approval })),
275
+ ...requeueables.map(stalled => ({ key: `r:${stalled.taskId}`, kind: "requeue", stalled })),
276
+ ...gaps.map(gap => ({ key: `g:${gap.key.replace(/[^A-Za-z0-9._:-]/g, "_")}`, kind: "gap", gap })),
277
+ ];
278
+ const remaining = queue.filter(one => !skipped.has(one.key));
279
+ const item = remaining[0] ?? null;
280
+ const csrf = who.via === "cookie" ? who.session.csrf : "";
281
+ if (item !== null && item.kind === "approval") {
282
+ // The card restates the digest-bound terms, so the nonce may be
283
+ // minted here — same rule as the task screen, same binding.
284
+ const ref = store.lookupRef(item.approval.taskId);
285
+ const scope = store.getScope(item.approval.taskId);
286
+ const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
287
+ const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
288
+ // The same agents block the task page and chat sign under (v48),
289
+ // and the same consent door: closed, it mints no nonce.
290
+ const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
291
+ const nonce = who.via === "cookie" && consentDoorOf(scope, route).open ? mintApprovalNonce(who.name, item.approval.taskId, approvalDigest) : "";
287
292
  return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
288
- item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
289
- approvalDigest: null, route: null,
290
- remaining: remaining.length, skipped: [...skipped], now,
293
+ item, scope,
294
+ planDocument: planView?.document ?? null,
295
+ planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
296
+ approvalDigest,
297
+ deliverable: ref?.deliverable ?? "branch",
298
+ route,
299
+ csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
291
300
  }));
292
301
  }
293
- if (url.pathname === "/board") {
294
- // scope=all is the rolled-up view: every project this server was
295
- // allowed to serve, on one board. The ceiling still rules row by row
296
- // (rowVisible, the same predicate as every list) — a repo outside
297
- // the server's configuration never renders a card, whatever the
298
- // database holds. Unplaced work (repo NULL) appears: it dispatches
299
- // anywhere, so every board honestly owns it.
300
- const all = url.searchParams.get("scope") === "all";
301
- // Roll-up admission happens BEFORE the query limit (Codex round 2,
302
- // finding 11) — and root ceilings enumerate too, through the STORED
303
- // repos that pass the ceiling (attended review, finding 3); the
304
- // per-row visible() re-check below stays either way.
305
- const admission = all ? admissionList() : null;
306
- const snapshot = store.boardScoped(all ? null : project, now, 200, admission);
307
- const admitted = all
308
- ? snapshot.tasks.filter(facts => facts.repo === null || visible(facts.repo))
309
- : snapshot.tasks;
310
- const done = all
311
- ? snapshot.done.filter(row => row.repo === null || visible(row.repo))
312
- : snapshot.done;
313
- // A blocker may live in a repo this server must not speak about —
314
- // redact its state before the pure classifier composes a sentence
315
- // from it (Codex round 2, finding 12). The dependency's NAME stays:
316
- // the edge belongs to the visible task; the other project's live
317
- // status does not.
318
- const cards = admitted.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo)
319
- ? { ...facts, blockerState: null }
320
- : facts, now));
321
- // "Since you last looked": what concluded between this session's
322
- // previous full board read and now. Fragment polls never move the
323
- // anchor — an open tab is not a person looking.
324
- let delta = null;
325
- if (who.via === "cookie" && url.searchParams.get("fragment") !== "1") {
326
- const prev = who.session.sawBoardAt;
327
- if (prev !== null && now.getTime() - prev > 5 * 60_000) {
328
- const sinceIso = new Date(prev).toISOString();
329
- const runs = store.runsSinceScoped(sinceIso, all ? null : project).filter(one => all ? one.taskId !== "" : true);
330
- delta = {
331
- agoMinutes: Math.round((now.getTime() - prev) / 60_000),
332
- built: runs.filter(one => one.outcome === "built" || one.outcome === "no-change").length,
333
- failed: runs.filter(one => one.outcome === "failed").length,
334
- questions: store.listDecisionsScoped(all ? null : project).filter(one => one.createdAt >= sinceIso && one.state !== "answered").length,
335
- };
336
- if (delta.built === 0 && delta.failed === 0 && delta.questions === 0)
337
- delta = null;
338
- }
339
- who.session.sawBoardAt = now.getTime();
340
- }
341
- const buildingCount = cards.filter(card => card.lane === "building").length;
342
- // A completed task whose proof is short or refuted and not yet
343
- // accepted (Priority 2) reads "needs verification", not done — the
344
- // board's once-and-only-once rule holds because this split is the
345
- // ONE place a done row becomes either lane; the done-lane render
346
- // below never sees the rows filtered out here.
347
- const unverifiedDone = done.filter(row => (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted);
348
- const verifiedDone = done.filter(row => !unverifiedDone.includes(row));
349
- const unverifiedCards = unverifiedDone.map(row => {
350
- // v40: the SAME chip gains one more word when a repair chain
351
- // exists for this row's own run — never a second card.
352
- const chain = row.runId === null ? null : store.repairChainFor(row.runId);
353
- const repairChain = chain === null
354
- ? null
355
- : {
356
- attempt: chain.attempt,
357
- outcome: chain.outcome,
358
- approved: chain.draftTask !== null && (store.getScope(chain.draftTask)?.approvedAt ?? null) !== null,
359
- };
360
- return attentionCardForUnverifiedDone({
361
- taskId: row.taskId,
362
- title: row.title,
363
- repo: row.repo,
364
- completedAt: row.completedAt,
365
- proofVerdict: row.proofVerdict,
366
- proofMatrix: row.proofMatrix,
367
- repairChain,
368
- });
369
- });
370
- const laneCards = [...cards, ...unverifiedCards];
371
- const body = boardBody({ cards: laneCards, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
372
- if (url.searchParams.get("fragment") === "1") {
373
- // The live region alone — the in-page swapper's diet. Same auth,
374
- // same ceiling, no shell, no scripts (finding 2).
375
- return respond(response, 200, "text/html; charset=utf-8", body);
376
- }
377
- if (url.searchParams.get("view") === "order") {
378
- // The board's ORDER view (operator request): the same screen, flipped
379
- // to dispatch order with the queue's drag handles. Reordering and
380
- // reserving are scheduling, not authority, so this is the one view
381
- // where a drag does anything; the state view stays a view.
382
- const csrf = who.via === "cookie" ? who.session.csrf : "";
383
- const revision = who.via === "cookie" ? who.session.projectRevision : 0;
384
- const region = queueRegionFor(project, csrf, revision, now);
385
- return sendScreen(response, 200, screen("board", [
386
- `<h1>Board</h1>`,
387
- `<p class="meta board-view"><a href="/board">state</a> \u00b7 <strong>order</strong> <span class="meta">\u2014 drag to reorder, or onto a worker to reserve; the state view is where cards move on their own</span></p>`,
388
- `<div id="queue-region">${region}</div>`,
389
- `<p class="meta" id="queue-region-stamp"></p>`,
390
- ].join("\n"), { chrome: chromeFor(project, "board"), functional: { script: queueScript(), fetches: true } }));
302
+ return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
303
+ item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
304
+ approvalDigest: null, route: null,
305
+ remaining: remaining.length, skipped: [...skipped], now,
306
+ }));
307
+ }
308
+ async function showBoard(ctx) {
309
+ const { url, who, response, now, project } = ctx;
310
+ // scope=all is the rolled-up view: every project this server was
311
+ // allowed to serve, on one board. The ceiling still rules row by row
312
+ // (rowVisible, the same predicate as every list) — a repo outside
313
+ // the server's configuration never renders a card, whatever the
314
+ // database holds. Unplaced work (repo NULL) appears: it dispatches
315
+ // anywhere, so every board honestly owns it.
316
+ const all = url.searchParams.get("scope") === "all";
317
+ // Roll-up admission happens BEFORE the query limit (Codex round 2,
318
+ // finding 11) — and root ceilings enumerate too, through the STORED
319
+ // repos that pass the ceiling (attended review, finding 3); the
320
+ // per-row visible() re-check below stays either way.
321
+ const admission = all ? admissionList() : null;
322
+ const snapshot = store.boardScoped(all ? null : project, now, 200, admission);
323
+ const admitted = all
324
+ ? snapshot.tasks.filter(facts => facts.repo === null || visible(facts.repo))
325
+ : snapshot.tasks;
326
+ const done = all
327
+ ? snapshot.done.filter(row => row.repo === null || visible(row.repo))
328
+ : snapshot.done;
329
+ // A blocker may live in a repo this server must not speak about —
330
+ // redact its state before the pure classifier composes a sentence
331
+ // from it (Codex round 2, finding 12). The dependency's NAME stays:
332
+ // the edge belongs to the visible task; the other project's live
333
+ // status does not.
334
+ const cards = admitted.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo)
335
+ ? { ...facts, blockerState: null }
336
+ : facts, now));
337
+ // "Since you last looked": what concluded between this session's
338
+ // previous full board read and now. Fragment polls never move the
339
+ // anchor — an open tab is not a person looking.
340
+ let delta = null;
341
+ if (who.via === "cookie" && url.searchParams.get("fragment") !== "1") {
342
+ const prev = who.session.sawBoardAt;
343
+ if (prev !== null && now.getTime() - prev > 5 * 60_000) {
344
+ const sinceIso = new Date(prev).toISOString();
345
+ const runs = store.runsSinceScoped(sinceIso, all ? null : project).filter(one => all ? one.taskId !== "" : true);
346
+ delta = {
347
+ agoMinutes: Math.round((now.getTime() - prev) / 60_000),
348
+ built: runs.filter(one => one.outcome === "built" || one.outcome === "no-change").length,
349
+ failed: runs.filter(one => one.outcome === "failed").length,
350
+ questions: store.listDecisionsScoped(all ? null : project).filter(one => one.createdAt >= sinceIso && one.state !== "answered").length,
351
+ };
352
+ if (delta.built === 0 && delta.failed === 0 && delta.questions === 0)
353
+ delta = null;
391
354
  }
392
- const regionBody = `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
393
- return sendScreen(response, 200, screen("board", regionBody, {
394
- chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
395
- functional: { script: regionScript("board-region", "1", buildingCount > 0 ? 10 : 30), fetches: true },
396
- }));
355
+ who.session.sawBoardAt = now.getTime();
397
356
  }
398
- if (url.pathname === "/review") {
399
- // The review cockpit (Priority 5): every visible COMPLETED task,
400
- // ranked by review priority, with one selected result projected
401
- // from the records the task, run, and done pages already read —
402
- // scope, plan, run, artifacts, verdict, publication. The
403
- // ranking is a labeled presentation aid: it never rewrites the
404
- // stored verdict, and the sealed patch downloads exactly as stored.
405
- // Admission binds BEFORE the SQL limit (the done page's own rule),
406
- // and every row is re-proved against the ceiling before ranking.
407
- const resultRow = (row) => {
408
- const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
409
- return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
410
- assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
411
- };
412
- const rows = familiesInView(project, { states: ["done"], limit: REVIEW_QUEUE_CAP }).flatMap(family => {
413
- const row = completedRowFor(family.current.id, project);
414
- return row === null ? [] : [{ ...resultRow(row), title: family.root.title }];
415
- });
416
- const ranked = rankReviewQueue(rows);
417
- const wanted = url.searchParams.get("result");
418
- const wantedId = wanted === null || wanted.length === 0 || wanted.length > 64 || hasForbiddenControls(wanted) ? null : wanted;
419
- const inQueue = wantedId === null ? null : ranked.find(one => one.taskId === wantedId) ?? null;
420
- // A stable deep link outlives the queue window (v2 review, comment
421
- // 1): a completion older than the newest REVIEW_QUEUE_CAP resolves
422
- // directly — re-proved as done, in this project, and inside the
423
- // ceiling — so an old receipt never reads as unfinished or foreign.
424
- // The ranked queue itself stays bounded and says so.
425
- const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
426
- const beyondRow = beyond === null ? null : resultRow(beyond);
427
- const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
428
- const expectedRun = url.searchParams.get("run");
429
- const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
430
- // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
431
- const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
432
- const attemptRow = attempt === null ? null : resultRow(attempt);
433
- const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
434
- const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
435
- const csrf = who.via === "cookie" ? who.session.csrf : "";
436
- // An exact link to a result this person can't see (or that never existed) reads the same either way.
437
- if (expectedRun !== null && wantedId !== null && chosen === null)
438
- return refuse(response, who, 404, "No such result in your projects.", "/work");
439
- if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
440
- return sendScreen(response, 409, screen("Result changed", '<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") }));
441
- }
442
- const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
443
- // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
444
- // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
445
- // a reason.
446
- const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
447
- const failedTask = selected === null ? null : store.getTask(selected.taskId);
448
- if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
449
- const attemptRun = store.getRun(selected.run.id);
450
- const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
451
- const latest = latestFinishedAttempt(runs);
452
- // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
453
- const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
454
- ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
455
- // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
456
- const family = delivered ? familyOf(selected.taskId) : null;
457
- const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
458
- runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
459
- ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
460
- if (attemptRun !== null) {
461
- // A delivered result didn't fail itself: the task's failure is its latest attempt's.
462
- const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
463
- selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
464
- }
465
- }
466
- const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
467
- queue: ranked,
468
- queueCap: REVIEW_QUEUE_CAP,
469
- selected,
470
- beyondQueue: beyond !== null && attemptRow === null,
471
- // A deep link to a result this console cannot show — not done,
472
- // not admitted, or never existed — says so in one sentence and
473
- // shows the top of the queue; the three cases read identically.
474
- missing: wantedId !== null && chosen === null ? wantedId : null,
475
- csrf,
476
- canRetryReview: who.via === "cookie" && who.role === "approver",
477
- noted: url.searchParams.get("noted") !== null,
478
- refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
479
- tab: parseResultTab(url.searchParams.get("tab")),
480
- user: who.name,
481
- now,
357
+ // A completed task whose proof is short or refuted and not yet
358
+ // accepted (Priority 2) reads "needs verification", not done — the
359
+ // board's once-and-only-once rule holds because this split is the
360
+ // ONE place a done row becomes either lane; the done-lane render
361
+ // below never sees the rows filtered out here.
362
+ const unverifiedDone = done.filter(row => (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted);
363
+ const verifiedDone = done.filter(row => !unverifiedDone.includes(row));
364
+ const unverifiedCards = unverifiedDone.map(row => {
365
+ // v40: the SAME chip gains one more word when a repair chain
366
+ // exists for this row's own run — never a second card.
367
+ const chain = row.runId === null ? null : store.repairChainFor(row.runId);
368
+ const repairChain = chain === null
369
+ ? null
370
+ : {
371
+ attempt: chain.attempt,
372
+ outcome: chain.outcome,
373
+ approved: chain.draftTask !== null && (store.getScope(chain.draftTask)?.approvedAt ?? null) !== null,
374
+ };
375
+ return attentionCardForUnverifiedDone({
376
+ taskId: row.taskId,
377
+ title: row.title,
378
+ repo: row.repo,
379
+ completedAt: row.completedAt,
380
+ proofVerdict: row.proofVerdict,
381
+ proofMatrix: row.proofMatrix,
382
+ repairChain,
482
383
  });
483
- // The result's task conversation, docked beside it (v77); messages
484
- // sent here carry the result being viewed.
485
- const reviewFocus = selected === null ? null : taskChatFocus(selected.taskId, now, who, { mintNonce: false });
486
- const reviewDocked = reviewFocus === null || selected === null ? null
487
- : dockedConversation(who, reviewFocus, null, now, reviewHref(selected.taskId), selected.run !== null && reviewFocus.family.versions.some(one => one.refId === store.getRun(selected.run.id)?.taskRef) ? selected.run.id : null);
488
- if (reviewDocked !== null)
489
- reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
490
- return sendScreen(response, 200, reviewPage);
491
- }
492
- if (url.pathname === "/done") {
493
- return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
494
- }
495
- if (url.pathname === "/tasks") {
496
- const wanted = url.searchParams.get("state");
497
- if (wanted !== null && !TASK_STATES.includes(wanted)) {
498
- return refuse(response, who, 400, "no such state", "/tasks");
499
- }
500
- // ?template=<name> pre-fills the add form from the shipped library —
501
- // a pre-filled form and nothing more; the submission path is the
502
- // same guarded handler either way.
503
- const fromTemplate = url.searchParams.get("template");
504
- const picked = fromTemplate === null ? null : templateByName(fromTemplate);
505
- const prefill = picked !== null && picked.kind === "task"
506
- ? {
507
- title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "),
508
- acceptance: acceptanceToLines(picked.acceptance).join("\n"),
509
- }
510
- : null;
511
- const csrf = who.via === "cookie" ? who.session.csrf : "";
512
- return sendScreen(response, 200, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project, wanted === null ? undefined : wanted).slice(0, 200), wanted, csrf, null, project, prefill, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
384
+ });
385
+ const laneCards = [...cards, ...unverifiedCards];
386
+ const body = boardBody({ cards: laneCards, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
387
+ if (url.searchParams.get("fragment") === "1") {
388
+ // The live region alone — the in-page swapper's diet. Same auth,
389
+ // same ceiling, no shell, no scripts (finding 2).
390
+ return respond(response, 200, "text/html; charset=utf-8", body);
513
391
  }
514
- if (url.pathname === "/queue") {
392
+ if (url.searchParams.get("view") === "order") {
393
+ // The board's ORDER view (operator request): the same screen, flipped
394
+ // to dispatch order with the queue's drag handles. Reordering and
395
+ // reserving are scheduling, not authority, so this is the one view
396
+ // where a drag does anything; the state view stays a view.
515
397
  const csrf = who.via === "cookie" ? who.session.csrf : "";
516
398
  const revision = who.via === "cookie" ? who.session.projectRevision : 0;
517
- // Column headers read one thing beyond the queue snapshot: live claims
518
- // in THIS project, per worker (the capacity is global), so the header
519
- // names both and never a ratio.
520
- if (url.searchParams.get("fragment") === "1") {
521
- return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
399
+ const region = queueRegionFor(project, csrf, revision, now);
400
+ return sendScreen(response, 200, screen("board", joinHtml([
401
+ html `<h1>Board</h1>`,
402
+ html `<p class="meta board-view"><a href="/board">state</a> \u00b7 <strong>order</strong> <span class="meta">\u2014 drag to reorder, or onto a worker to reserve; the state view is where cards move on their own</span></p>`,
403
+ html `<div id="queue-region">${region}</div>`,
404
+ html `<p class="meta" id="queue-region-stamp"></p>`,
405
+ ], "\n"), { chrome: chromeFor(project, "board"), functional: { script: queueScript(), fetches: true } }));
406
+ }
407
+ const regionBody = html `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
408
+ return sendScreen(response, 200, screen("board", regionBody, {
409
+ chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
410
+ functional: { script: regionScript("board-region", "1"), fetches: true },
411
+ }));
412
+ }
413
+ async function showReview(ctx) {
414
+ const { url, who, response, now, project, chosenProject } = ctx;
415
+ // The review cockpit (Priority 5): every visible COMPLETED task,
416
+ // ranked by review priority, with one selected result projected
417
+ // from the records the task, run, and done pages already read —
418
+ // scope, plan, run, artifacts, verdict, publication. The
419
+ // ranking is a labeled presentation aid: it never rewrites the
420
+ // stored verdict, and the sealed patch downloads exactly as stored.
421
+ // Admission binds BEFORE the SQL limit (the done page's own rule),
422
+ // and every row is re-proved against the ceiling before ranking.
423
+ const resultRow = (row) => {
424
+ const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
425
+ return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
426
+ assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
427
+ };
428
+ const rows = familiesInView(project, { states: ["done"], limit: REVIEW_QUEUE_CAP }).flatMap(family => {
429
+ const row = completedRowFor(family.current.id, project);
430
+ return row === null ? [] : [{ ...resultRow(row), title: family.root.title }];
431
+ });
432
+ const ranked = rankReviewQueue(rows);
433
+ const wanted = url.searchParams.get("result");
434
+ const wantedId = wanted === null || wanted.length === 0 || wanted.length > 64 || hasForbiddenControls(wanted) ? null : wanted;
435
+ const inQueue = wantedId === null ? null : ranked.find(one => one.taskId === wantedId) ?? null;
436
+ // A stable deep link outlives the queue window (v2 review, comment
437
+ // 1): a completion older than the newest REVIEW_QUEUE_CAP resolves
438
+ // directly — re-proved as done, in this project, and inside the
439
+ // ceiling — so an old receipt never reads as unfinished or foreign.
440
+ // The ranked queue itself stays bounded and says so.
441
+ const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
442
+ const beyondRow = beyond === null ? null : resultRow(beyond);
443
+ const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
444
+ const expectedRun = url.searchParams.get("run");
445
+ const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
446
+ // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
447
+ const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
448
+ const attemptRow = attempt === null ? null : resultRow(attempt);
449
+ const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
450
+ const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
451
+ const csrf = who.via === "cookie" ? who.session.csrf : "";
452
+ // An exact link to a result this person can't see (or that never existed) reads the same either way.
453
+ if (expectedRun !== null && wantedId !== null && chosen === null)
454
+ return refuse(response, who, 404, "No such result in your projects.", "/work");
455
+ if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
456
+ 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") }));
457
+ }
458
+ const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
459
+ // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
460
+ // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
461
+ // a reason.
462
+ const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
463
+ const failedTask = selected === null ? null : store.getTask(selected.taskId);
464
+ if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
465
+ const attemptRun = store.getRun(selected.run.id);
466
+ const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
467
+ const latest = latestFinishedAttempt(runs);
468
+ // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
469
+ const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
470
+ ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
471
+ // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
472
+ const family = delivered ? familyOf(selected.taskId) : null;
473
+ const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
474
+ runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
475
+ ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
476
+ if (attemptRun !== null) {
477
+ // A delivered result didn't fail itself: the task's failure is its latest attempt's.
478
+ const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
479
+ selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
522
480
  }
523
- // The queue is the board's order view (reduction pass §1): the URL
524
- // keeps answering, as a redirect, so nothing anyone bookmarked 404s.
525
- return redirect(response, QUEUE_VIEW);
526
481
  }
527
- if (url.pathname === "/tasks/new") {
528
- const csrf = who.via === "cookie" ? who.session.csrf : "";
529
- const revision = who.via === "cookie" ? who.session.projectRevision : 0;
530
- const chainable = store
531
- .listTasksScoped(project, undefined, 100, null)
532
- .filter(one => one.state !== "done" && one.state !== "cancelled" && visible(one.repo))
533
- .map(one => ({ id: one.id, title: one.title }));
534
- const chrome = chromeFor(project, "tasks");
535
- return sendScreen(response, 200, newTaskPage(chrome, project, csrf, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
536
- }
537
- const taskLive = matchTaskPath(url.pathname, "/live$");
538
- if (taskLive !== null) {
539
- // The live task page: a nudge the moment the task changes, and who else
540
- // has it open. Hints only — the page reads the task the usual way.
541
- if (who.via !== "cookie")
542
- return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
543
- const family = familyOf(taskLive.taskId);
544
- if (family === null || family.problem !== null)
545
- return respond(response, 404, "application/json", JSON.stringify({ error: "task" }));
546
- response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
547
- const name = who.name, repo = family.root.repo;
548
- taskRooms.join(family.root.id, {
549
- name, response,
550
- // Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
551
- valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
552
- });
553
- request.once("close", () => { if (!response.writableEnded)
554
- response.end(); });
555
- return;
482
+ const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
483
+ queue: ranked,
484
+ queueCap: REVIEW_QUEUE_CAP,
485
+ selected,
486
+ beyondQueue: beyond !== null && attemptRow === null,
487
+ // A deep link to a result this console cannot show — not done,
488
+ // not admitted, or never existed — says so in one sentence and
489
+ // shows the top of the queue; the three cases read identically.
490
+ missing: wantedId !== null && chosen === null ? wantedId : null,
491
+ csrf,
492
+ canRetryReview: who.via === "cookie" && who.role === "approver",
493
+ noted: url.searchParams.get("noted") !== null,
494
+ refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
495
+ tab: parseResultTab(url.searchParams.get("tab")),
496
+ user: who.name,
497
+ now,
498
+ });
499
+ // The result's task conversation, docked beside it (v77); messages
500
+ // sent here carry the result being viewed.
501
+ const reviewFocus = selected === null ? null : taskChatFocus(selected.taskId, now, who, { mintNonce: false });
502
+ const reviewDocked = reviewFocus === null || selected === null ? null
503
+ : dockedConversation(who, reviewFocus, null, now, reviewHref(selected.taskId), selected.run !== null && reviewFocus.family.versions.some(one => one.refId === store.getRun(selected.run.id)?.taskRef) ? selected.run.id : null);
504
+ if (reviewDocked !== null)
505
+ reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
506
+ return sendScreen(response, 200, reviewPage);
507
+ }
508
+ async function showDone(ctx) {
509
+ const { response, project } = ctx;
510
+ return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
511
+ }
512
+ async function showTasks(ctx) {
513
+ const { url, who, response, project } = ctx;
514
+ const wanted = url.searchParams.get("state");
515
+ if (wanted !== null && !TASK_STATES.includes(wanted)) {
516
+ return refuse(response, who, 400, "no such state", "/tasks");
517
+ }
518
+ // ?template=<name> pre-fills the add form from the shipped library —
519
+ // a pre-filled form and nothing more; the submission path is the
520
+ // same guarded handler either way.
521
+ const fromTemplate = url.searchParams.get("template");
522
+ const picked = fromTemplate === null ? null : templateByName(fromTemplate);
523
+ const prefill = picked !== null && picked.kind === "task"
524
+ ? {
525
+ title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "),
526
+ acceptance: acceptanceToLines(picked.acceptance).join("\n"),
527
+ }
528
+ : null;
529
+ return sendScreen(response, 200, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project, wanted === null ? undefined : wanted).slice(0, 200), wanted, null, project, prefill, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
530
+ }
531
+ async function showQueue(ctx) {
532
+ const { url, who, response, project } = ctx;
533
+ const csrf = who.via === "cookie" ? who.session.csrf : "";
534
+ const revision = who.via === "cookie" ? who.session.projectRevision : 0;
535
+ // Column headers read one thing beyond the queue snapshot: live claims
536
+ // in THIS project, per worker (the capacity is global), so the header
537
+ // names both and never a ratio.
538
+ if (url.searchParams.get("fragment") === "1") {
539
+ return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
556
540
  }
541
+ // The queue is the board's order view (reduction pass §1): the URL
542
+ // keeps answering, as a redirect, so nothing anyone bookmarked 404s.
543
+ return redirect(response, QUEUE_VIEW);
544
+ }
545
+ async function showNewTask(ctx) {
546
+ const { who, response, project } = ctx;
547
+ const revision = who.via === "cookie" ? who.session.projectRevision : 0;
548
+ const chainable = store
549
+ .listTasksScoped(project, undefined, 100, null)
550
+ .filter(one => one.state !== "done" && one.state !== "cancelled" && visible(one.repo))
551
+ .map(one => ({ id: one.id, title: one.title }));
552
+ const chrome = chromeFor(project, "tasks");
553
+ return sendScreen(response, 200, newTaskPage(chrome, project, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
554
+ }
555
+ async function showTask(ctx) {
556
+ const { url, who, response } = ctx;
557
557
  const task = matchTaskPath(url.pathname, "");
558
- if (task !== null) {
559
- const family = familyOf(task.taskId);
560
- if (family === null)
561
- return refuse(response, who, 404, "no such task", "/tasks");
562
- const version = url.searchParams.get("version");
563
- if (version !== null && !family.versions.some(one => one.id === version))
564
- return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
565
- // A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
566
- if (task.taskId !== family.root.id)
567
- return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
568
- return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
569
- }
570
- // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
558
+ if (task === null)
559
+ return noPage(who, response);
560
+ const family = familyOf(task.taskId);
561
+ if (family === null)
562
+ return refuse(response, who, 404, "no such task", "/tasks");
563
+ const version = url.searchParams.get("version");
564
+ if (version !== null && !family.versions.some(one => one.id === version))
565
+ return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
566
+ // A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
567
+ if (task.taskId !== family.root.id)
568
+ return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
569
+ return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
570
+ }
571
+ // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
572
+ async function showTaskEvidence(ctx) {
573
+ const { url, who, response, now } = ctx;
571
574
  const evidence = matchTaskPath(url.pathname, "/evidence$");
572
- if (evidence !== null) {
573
- const access = workAccess();
574
- const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
575
- if (family === null || !visible(family.root.repo))
576
- return refuse(response, who, 404, "no such task", "/tasks");
577
- if (evidence.taskId !== family.root.id)
578
- return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
579
- const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
580
- if (pack === null)
581
- return refuse(response, who, 404, "no such task", "/tasks");
582
- if (url.searchParams.get("format") === "json") {
583
- response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
584
- "cache-control": "no-store", "x-content-type-options": "nosniff" });
585
- return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
586
- }
587
- return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
575
+ if (evidence === null)
576
+ return noPage(who, response);
577
+ const access = workAccess();
578
+ const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
579
+ if (family === null || !visible(family.root.repo))
580
+ return refuse(response, who, 404, "no such task", "/tasks");
581
+ if (evidence.taskId !== family.root.id)
582
+ return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
583
+ const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
584
+ if (pack === null)
585
+ return refuse(response, who, 404, "no such task", "/tasks");
586
+ if (url.searchParams.get("format") === "json") {
587
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
588
+ "cache-control": "no-store", "x-content-type-options": "nosniff" });
589
+ return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
588
590
  }
589
- if (url.pathname === "/runs") {
590
- const raw = url.searchParams.get("before");
591
- let before = null;
592
- if (raw !== null) {
593
- if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
594
- return refuse(response, who, 400, "that cursor is not a page", "/runs");
595
- }
596
- before = Number(raw);
597
- }
598
- const rows = store.listRunsBefore(before, RUNS_PAGE, project);
599
- const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
600
- const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
601
- return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
591
+ return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
592
+ }
593
+ async function showRuns(ctx) {
594
+ const { url, who, response, project } = ctx;
595
+ const raw = url.searchParams.get("before");
596
+ let before = null;
597
+ if (raw !== null) {
598
+ if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
599
+ return refuse(response, who, 400, "that cursor is not a page", "/runs");
600
+ }
601
+ before = Number(raw);
602
602
  }
603
+ const rows = store.listRunsBefore(before, RUNS_PAGE, project);
604
+ const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
605
+ const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
606
+ return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
607
+ }
608
+ async function showRun(ctx) {
609
+ const { url, who, response, now, project } = ctx;
603
610
  const run = /^\/r\/([0-9]{1,15})$/.exec(url.pathname);
604
- if (run !== null) {
605
- const found = store.getRun(Number(run[1]));
606
- if (found === null || !runVisible(found)) {
607
- return refuse(response, who, 404, "no such run", "/runs");
608
- }
609
- const taskId = store.externalIdFor(found.taskRef) ?? "?";
610
- const running = runIsLive(found);
611
- if (url.searchParams.get("fragment") === "check") {
612
- response.setHeader("cache-control", "no-store");
613
- return respond(response, 200, "text/html; charset=utf-8", checkProgressHtml(store.checkProgress(found.id)));
614
- }
615
- if (url.searchParams.get("fragment") === "facts") {
616
- // The facts region alone — same auth and ceiling as the page; a
617
- // finished or no-longer-live run says so rather than growing forms
618
- // (finding 5; round-4 finding 15).
619
- return respond(response, 200, "text/html; charset=utf-8", runFactsFragment(found, taskId, running, store.runRoute(found.id)));
620
- }
621
- if (url.searchParams.get("fragment") === "peek") {
622
- // The live peek: cookie sessions only (v2 §3), never stored, never
623
- // cached by anything downstream.
624
- if (who.via !== "cookie")
625
- return respond(response, 403, "text/plain; charset=utf-8", "the live peek is a browser session's view");
626
- const peeked = await peekFragment(found.id, who.session.csrf, options.editorLinks !== undefined && found.runner === options.localRunner && who.session.editorLinks === true);
627
- response.setHeader("cache-control", "no-store");
628
- if (peeked.retryAfter !== undefined)
629
- response.setHeader("retry-after", String(peeked.retryAfter));
630
- return respond(response, peeked.status, "text/html; charset=utf-8", peeked.body);
631
- }
632
- if (url.searchParams.get("fragment") === "transcript") {
633
- // The live transcript window (arc 1): raw sanitized TEXT as JSON —
634
- // the sink is textContent, never innerHTML. Guards, enumerated:
635
- // cookie session; run visible (proved above); this machine's
636
- // runner asserted; byte offsets validated; the file read through
637
- // its own descriptor with the exact numeric-id name. Display
638
- // state, not evidence — and never cached.
639
- response.setHeader("cache-control", "no-store");
640
- if (who.via !== "cookie")
641
- return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
642
- if (options.localRunner === undefined) {
643
- return respond(response, 200, "application/json", JSON.stringify({ error: "off" }));
644
- }
645
- const fromRaw = url.searchParams.get("from") ?? "0";
646
- const from = /^[0-9]{1,15}$/.test(fromRaw) ? Number(fromRaw) : Number.NaN;
647
- if (!Number.isSafeInteger(from) || from < 0) {
648
- return respond(response, 400, "application/json", JSON.stringify({ error: "offset" }));
649
- }
650
- const live = runIsLive(found);
651
- // The final drain: a finished run's tail — including a torn last
652
- // line — may be read until the sweep removes the file.
653
- const window = readLiveWindow(evidenceRoot, found.id, from, !live);
654
- if (!window.ok) {
655
- if (window.reason === "replaced") {
656
- return respond(response, 409, "application/json", JSON.stringify({ error: "replaced" }));
657
- }
658
- // Missing or unreadable: nothing to show. Final only when the
659
- // run can never write again.
660
- return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: !live }));
661
- }
662
- // Re-proof after the read (peek discipline): the run row still says
663
- // what admission said, or nothing is shown.
664
- const again = store.getRun(found.id);
665
- if (again === null || !runVisible(again)) {
666
- return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: true }));
667
- }
668
- return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
669
- }
670
- // One result page (2026-10-02): a builder's finished result opens on
671
- // its task's result page, titled with the task. The raw run record
672
- // waits there under Details, and here with ?record=1.
673
- if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
674
- completedRowFor(taskId, null)?.runId === found.id) {
675
- const tab = parseResultTab(url.searchParams.get("tab"));
676
- return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
677
- }
678
- // A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
679
- if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
680
- const tab = parseResultTab(url.searchParams.get("tab"));
681
- return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
682
- }
683
- // Pollers exist only for a LIVE run — an orphaned null-outcome run
684
- // would otherwise be refetched forever (finding 15). The nonce is
685
- // sendScreen's business now.
686
- const artifacts = store.artifactsFor(found.id);
687
- return sendScreen(response, 200, runPage(chromeFor(project, "runs", runListPane(project, found.id)), found, taskId, artifacts, terminalDiffView(artifacts, evidenceRoot), store.notesForRun(found.id), who.via === "cookie" ? who.session.csrf : "", store.liveDiffComments(found.id).filter(isRevisionFeedback), (() => {
688
- const publication = store.publicationForRun(found.id);
689
- return publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)
690
- ? { pr: publication.prNumber }
691
- : null;
692
- })(), !running
693
- ? undefined
694
- : regionScript("run-facts", "facts", 10) +
695
- (options.localRunner === undefined ? "" : regionScript("run-peek", "peek", 15)) +
696
- (options.localRunner === undefined ? "" : transcriptScript()), options.localRunner !== undefined, running,
697
- // Editor links (arc 6): three statements align or nothing renders —
698
- // the deployment capability, THIS machine's runner owning the run,
699
- // and the session's own device-side yes.
700
- options.editorLinks !== undefined &&
701
- options.localRunner !== undefined &&
702
- found.runner === options.localRunner &&
703
- who.via === "cookie" &&
704
- who.session.editorLinks === true &&
705
- // A reviewer run (v29) never had a checkout — no files to open.
706
- found.worktree !== null
707
- ? { worktree: found.worktree }
708
- : null, options.editorLinks !== undefined &&
709
- options.localRunner !== undefined &&
710
- found.runner === options.localRunner &&
711
- who.via === "cookie"
712
- ? { on: who.session.editorLinks === true }
713
- : null, url.searchParams.get("noted") !== null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
714
- // The shared result presentation (package 3) for a finished result.
715
- !running && (found.outcome === "built" || found.outcome === "no-change")
716
- ? { detail: resultDetailOf(found, who, now), tab: parseResultTab(url.searchParams.get("tab")), user: who.name, requestToken: randomBytes(16).toString("hex") }
717
- : null, store.getScope(taskId)?.digest ?? null));
611
+ if (run === null)
612
+ return noPage(who, response);
613
+ const found = store.getRun(Number(run[1]));
614
+ if (found === null || !runVisible(found)) {
615
+ return refuse(response, who, 404, "no such run", "/runs");
718
616
  }
719
- const runArtifact = /^\/r\/([0-9]{1,15})\/evidence\/([0-9]{1,15})$/.exec(url.pathname);
720
- if (runArtifact !== null) {
721
- return runEvidence(response, Number(runArtifact[1]), Number(runArtifact[2]));
617
+ const taskId = store.externalIdFor(found.taskRef) ?? "?";
618
+ const running = runIsLive(found);
619
+ if (url.searchParams.get("fragment") === "check") {
620
+ response.setHeader("cache-control", "no-store");
621
+ return respond(response, 200, "text/html; charset=utf-8", checkProgressHtml(store.checkProgress(found.id)));
622
+ }
623
+ if (url.searchParams.get("fragment") === "facts") {
624
+ // The facts region alone — same auth and ceiling as the page; a
625
+ // finished or no-longer-live run says so rather than growing forms
626
+ // (finding 5; round-4 finding 15).
627
+ return respond(response, 200, "text/html; charset=utf-8", runFactsFragment(found, taskId, running, store.runRoute(found.id)));
628
+ }
629
+ if (url.searchParams.get("fragment") === "peek") {
630
+ // The live peek: cookie sessions only (v2 §3), never stored, never
631
+ // cached by anything downstream.
632
+ if (who.via !== "cookie")
633
+ return respond(response, 403, "text/plain; charset=utf-8", "the live peek is a browser session's view");
634
+ const peeked = await peekFragment(found.id, who.session.csrf, options.editorLinks !== undefined && found.runner === options.localRunner && who.session.editorLinks === true);
635
+ response.setHeader("cache-control", "no-store");
636
+ if (peeked.retryAfter !== undefined)
637
+ response.setHeader("retry-after", String(peeked.retryAfter));
638
+ return respond(response, peeked.status, "text/html; charset=utf-8", peeked.body);
639
+ }
640
+ if (url.searchParams.get("fragment") === "transcript") {
641
+ // The live transcript window (arc 1): raw sanitized TEXT as JSON —
642
+ // the sink is textContent, never innerHTML. Guards, enumerated:
643
+ // cookie session; run visible (proved above); this machine's
644
+ // runner asserted; byte offsets validated; the file read through
645
+ // its own descriptor with the exact numeric-id name. Display
646
+ // state, not evidence — and never cached.
647
+ response.setHeader("cache-control", "no-store");
648
+ if (who.via !== "cookie")
649
+ return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
650
+ if (options.localRunner === undefined) {
651
+ return respond(response, 200, "application/json", JSON.stringify({ error: "off" }));
652
+ }
653
+ const fromRaw = url.searchParams.get("from") ?? "0";
654
+ const from = /^[0-9]{1,15}$/.test(fromRaw) ? Number(fromRaw) : Number.NaN;
655
+ if (!Number.isSafeInteger(from) || from < 0) {
656
+ return respond(response, 400, "application/json", JSON.stringify({ error: "offset" }));
657
+ }
658
+ const live = runIsLive(found);
659
+ // The final drain: a finished run's tail — including a torn last
660
+ // line — may be read until the sweep removes the file.
661
+ const window = readLiveWindow(evidenceRoot, found.id, from, !live);
662
+ if (!window.ok) {
663
+ if (window.reason === "replaced") {
664
+ return respond(response, 409, "application/json", JSON.stringify({ error: "replaced" }));
665
+ }
666
+ // Missing or unreadable: nothing to show. Final only when the
667
+ // run can never write again.
668
+ return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: !live }));
669
+ }
670
+ // Re-proof after the read (peek discipline): the run row still says
671
+ // what admission said, or nothing is shown.
672
+ const again = store.getRun(found.id);
673
+ if (again === null || !runVisible(again)) {
674
+ return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: true }));
675
+ }
676
+ return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
722
677
  }
723
- // v115: routines became scheduled flows; an old link to the list or to one routine lands on the flows page.
724
- if (url.pathname === "/routines" || /^\/routines\/[0-9]{1,15}$/.test(url.pathname))
725
- return redirect(response, "/flows");
678
+ // One result page (2026-10-02): a builder's finished result opens on
679
+ // its task's result page, titled with the task. The raw run record
680
+ // waits there under Details, and here with ?record=1.
681
+ if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
682
+ completedRowFor(taskId, null)?.runId === found.id) {
683
+ const tab = parseResultTab(url.searchParams.get("tab"));
684
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
685
+ }
686
+ // A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
687
+ if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
688
+ const tab = parseResultTab(url.searchParams.get("tab"));
689
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
690
+ }
691
+ // Pollers exist only for a LIVE run — an orphaned null-outcome run
692
+ // would otherwise be refetched forever (finding 15). The nonce is
693
+ // sendScreen's business now.
694
+ const artifacts = store.artifactsFor(found.id);
695
+ return sendScreen(response, 200, runPage(chromeFor(project, "runs", runListPane(project, found.id)), found, taskId, artifacts, terminalDiffView(artifacts, evidenceRoot), store.notesForRun(found.id), who.via === "cookie" ? who.session.csrf : "", store.liveDiffComments(found.id).filter(isRevisionFeedback), (() => {
696
+ const publication = store.publicationForRun(found.id);
697
+ return publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)
698
+ ? { pr: publication.prNumber }
699
+ : null;
700
+ })(), !running
701
+ ? undefined
702
+ : regionScript("run-facts", "facts") +
703
+ (options.localRunner === undefined ? "" : regionScript("run-peek", "peek")) +
704
+ (options.localRunner === undefined ? "" : transcriptScript()), options.localRunner !== undefined, running,
705
+ // Editor links (arc 6): three statements align or nothing renders —
706
+ // the deployment capability, THIS machine's runner owning the run,
707
+ // and the session's own device-side yes.
708
+ options.editorLinks !== undefined &&
709
+ options.localRunner !== undefined &&
710
+ found.runner === options.localRunner &&
711
+ who.via === "cookie" &&
712
+ who.session.editorLinks === true &&
713
+ // A reviewer run (v29) never had a checkout — no files to open.
714
+ found.worktree !== null
715
+ ? { worktree: found.worktree }
716
+ : null, options.editorLinks !== undefined &&
717
+ options.localRunner !== undefined &&
718
+ found.runner === options.localRunner &&
719
+ who.via === "cookie"
720
+ ? { on: who.session.editorLinks === true }
721
+ : null, url.searchParams.get("noted") !== null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
722
+ // The shared result presentation (package 3) for a finished result.
723
+ !running && (found.outcome === "built" || found.outcome === "no-change")
724
+ ? { detail: resultDetailOf(found, who, now), tab: parseResultTab(url.searchParams.get("tab")), user: who.name, requestToken: randomBytes(16).toString("hex") }
725
+ : null, store.getScope(taskId)?.digest ?? null));
726
+ }
727
+ async function showRunEvidence(ctx) {
728
+ const { url, who, response } = ctx;
729
+ const runArtifact = /^\/r\/([0-9]{1,15})\/evidence\/([0-9]{1,15})$/.exec(url.pathname);
730
+ if (runArtifact === null)
731
+ return noPage(who, response);
732
+ return runEvidence(response, Number(runArtifact[1]), Number(runArtifact[2]));
733
+ }
734
+ // v115: routines became scheduled flows; an old link to the list or to one routine lands on the flows page.
735
+ async function showRoutines(ctx) {
736
+ return redirect(ctx.response, "/flows");
737
+ }
738
+ async function showRoutine(ctx) {
739
+ return redirect(ctx.response, "/flows");
740
+ }
741
+ async function showDecision(ctx) {
742
+ const { url, who, response, now, project } = ctx;
726
743
  const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
727
- if (one !== null) {
728
- const decision = store.getDecision(Number(one[1]));
729
- if (decision === null)
730
- return refuse(response, who, 404, "no such decision");
731
- const run = store.getRun(decision.run);
732
- if (run !== null && !visible(taskRepoOf(run.taskRef))) {
733
- return refuse(response, who, 404, "no such decision");
734
- }
735
- const taskId = taskOf(store, decision);
736
- const decisionReturn = url.searchParams.get("return");
737
- const back = decisionReturn === null ? null : safeChatReturn(decisionReturn);
738
- return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
744
+ if (one === null)
745
+ return noPage(who, response);
746
+ const decision = store.getDecision(Number(one[1]));
747
+ if (decision === null)
748
+ return refuse(response, who, 404, "no such decision");
749
+ const run = store.getRun(decision.run);
750
+ if (run !== null && !visible(taskRepoOf(run.taskRef))) {
751
+ return refuse(response, who, 404, "no such decision");
739
752
  }
753
+ const taskId = taskOf(store, decision);
754
+ const decisionReturn = url.searchParams.get("return");
755
+ const back = decisionReturn === null ? null : safeChatReturn(decisionReturn);
756
+ return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
757
+ }
758
+ async function showDecisionEvidence(ctx) {
759
+ const { url, who, response } = ctx;
740
760
  const artifact = /^\/d\/([0-9]{1,15})\/evidence\/([0-9]{1,15})$/.exec(url.pathname);
741
- if (artifact !== null) {
742
- return decisionEvidence(response, Number(artifact[1]), Number(artifact[2]));
743
- }
744
- return refuse(response, who, 404, "There's no page at this address.", "/chat");
761
+ if (artifact === null)
762
+ return noPage(who, response);
763
+ return decisionEvidence(response, Number(artifact[1]), Number(artifact[2]));
745
764
  }
746
- async function post(ctx) {
747
- const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
748
- if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
749
- const body = readForm(posted, CONSOLE_FORMS.code);
750
- const wantsJson = request.headers.accept?.includes('application/json') === true;
751
- const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
752
- ? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
753
- : refuse(response, who, status, message, '/code');
754
- if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }) || store.isDemo())
755
- return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
756
- if (!runtime.coding)
757
- return fail(409, runtime.codingProblem || 'Open Toolroll on the machine with your installed coding agent.');
758
- if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
759
- return fail(400, 'Submit one value for each field.');
760
- const actor = { name: who.name, generation: who.session.generation };
761
- const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
762
- // Sprint 8: a coding session is Codex taking turns: the organisation policy decides whether it may (provider,
763
- // model, and a ceiling Codex can't run under), when it starts and on every action that makes it take another turn.
764
- const turnAction = /^\/code\/[a-f0-9]{32}\/(send|resume|recover|continue|answer)$/.test(url.pathname);
765
- if (url.pathname === '/code/start' || turnAction) {
766
- const stopped = store.sessionPolicyRefusal('codex', url.pathname === '/code/start' ? body.get('model')?.trim() || null : null, 'coding sessions');
767
- if (stopped !== null)
768
- return fail(403, stopped);
769
- }
770
- try {
771
- let id;
772
- if (url.pathname === '/code/start') {
773
- const repo = body.get('repo') ?? '';
774
- if (!permitted(repo))
775
- return fail(403, 'Choose a project available to your account.');
776
- if (!authenticateApprover(who, body.get('password') ?? '').ok)
777
- return fail(403, 'Enter your Toolroll password to authorize this coding session.');
778
- const session = await runtime.coding.start(actor, { repo, title: body.get('title') ?? '', model: body.get('model')?.trim() || null, prompt: body.get('prompt') ?? '', requestId: body.get('requestId') ?? '' });
779
- id = session.id;
780
- }
765
+ const codeAct = (verb) => async (ctx) => {
766
+ const { url, who, request, response, posted } = ctx;
767
+ const body = readForm(posted, CONSOLE_FORMS.code);
768
+ const wantsJson = request.headers.accept?.includes('application/json') === true;
769
+ const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
770
+ ? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
771
+ : refuse(response, who, status, message, '/code');
772
+ if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }) || store.isDemo())
773
+ return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
774
+ if (!runtime.coding)
775
+ return fail(409, runtime.codingProblem || 'Open Toolroll on the machine with your installed coding agent.');
776
+ if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
777
+ return fail(400, 'Submit one value for each field.');
778
+ const actor = { name: who.name, generation: who.session.generation };
779
+ const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
780
+ const starting = verb === 'start';
781
+ const sessionId = starting || verb === 'unknown' ? null : /^\/code\/([a-f0-9]{32})/.exec(url.pathname)[1];
782
+ // Sprint 8: a coding session is Codex taking turns: the organisation policy decides whether it may (provider,
783
+ // model, and a ceiling Codex can't run under), when it starts and on every action that makes it take another turn.
784
+ const turnAction = ['send', 'resume', 'recover', 'continue', 'answer'].includes(verb);
785
+ if (starting || turnAction) {
786
+ const stopped = store.sessionPolicyRefusal('codex', starting ? body.get('model')?.trim() || null : null, 'coding sessions');
787
+ if (stopped !== null)
788
+ return fail(403, stopped);
789
+ }
790
+ try {
791
+ let id;
792
+ if (starting) {
793
+ const repo = body.get('repo') ?? '';
794
+ if (!permitted(repo))
795
+ return fail(403, 'Choose a project available to your account.');
796
+ if (!authenticateApprover(who, body.get('password') ?? '').ok)
797
+ return fail(403, 'Enter your Toolroll password to authorize this coding session.');
798
+ const session = await runtime.coding.start(actor, { repo, title: body.get('title') ?? '', model: body.get('model')?.trim() || null, prompt: body.get('prompt') ?? '', requestId: body.get('requestId') ?? '' });
799
+ id = session.id;
800
+ }
801
+ else {
802
+ if (sessionId === null)
803
+ return fail(404, 'Coding action not found.');
804
+ id = sessionId;
805
+ const session = runtime.coding.get(id, actor);
806
+ if (!permitted(session.repo))
807
+ return fail(403, 'That project is outside your access.');
808
+ if (verb === 'ship') {
809
+ const made = createCodingHandoff(store, { sessionId: id, actor: who.name, repo: session.repo, base: body.get('base') ?? '', candidate: body.get('candidate') ?? '', title: body.get('title') ?? '', goal: body.get('goal') ?? '', acceptance: (body.get('acceptance') ?? '').split('\n').map(line => line.trim()).filter(Boolean).map((statement, index) => ({ id: `c${index + 1}`, statement, evidence: ['check', 'changed-path', ...(body.get('visual') === 'yes' ? ['screenshot'] : [])] })) });
810
+ return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, taskId: made.taskId })) : redirect(response, `/t/${encodeURIComponent(made.taskId)}`);
811
+ }
812
+ if (verb === 'send')
813
+ await runtime.coding.send(id, actor, body.get('prompt') ?? '', body.get('requestId') ?? '');
814
+ else if (verb === 'stop')
815
+ await runtime.coding.stop(id, actor);
816
+ else if (verb === 'resume')
817
+ await runtime.coding.resume(id, actor);
818
+ else if (verb === 'recover')
819
+ await runtime.coding.recover(id, actor);
820
+ else if (verb === 'continue')
821
+ await runtime.coding.continueSaved(id, actor);
781
822
  else {
782
- const match = /^\/code\/([a-f0-9]{32})\/(send|stop|resume|recover|continue|ship|answer)$/.exec(url.pathname);
783
- if (!match)
784
- return fail(404, 'Coding action not found.');
785
- id = match[1];
786
- const session = runtime.coding.get(id, actor);
787
- if (!permitted(session.repo))
788
- return fail(403, 'That project is outside your access.');
789
- if (match[2] === 'ship') {
790
- const made = createCodingHandoff(store, { sessionId: id, actor: who.name, repo: session.repo, base: body.get('base') ?? '', candidate: body.get('candidate') ?? '', title: body.get('title') ?? '', goal: body.get('goal') ?? '', acceptance: (body.get('acceptance') ?? '').split('\n').map(line => line.trim()).filter(Boolean).map((statement, index) => ({ id: `c${index + 1}`, statement, evidence: ['check', 'changed-path', ...(body.get('visual') === 'yes' ? ['screenshot'] : [])] })) });
791
- return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, taskId: made.taskId })) : redirect(response, `/t/${encodeURIComponent(made.taskId)}`);
792
- }
793
- if (match[2] === 'send')
794
- await runtime.coding.send(id, actor, body.get('prompt') ?? '', body.get('requestId') ?? '');
795
- else if (match[2] === 'stop')
796
- await runtime.coding.stop(id, actor);
797
- else if (match[2] === 'resume')
798
- await runtime.coding.resume(id, actor);
799
- else if (match[2] === 'recover')
800
- await runtime.coding.recover(id, actor);
801
- else if (match[2] === 'continue')
802
- await runtime.coding.continueSaved(id, actor);
803
- else {
804
- const token = body.get('requestId') ?? '';
805
- const pending = runtime.coding.snapshot(id, actor).requests.find(r => r.id === token);
806
- if (!pending)
807
- return fail(409, 'This request is no longer waiting for an answer.');
808
- const decision = body.get('decision');
809
- if (decision !== 'accept' && decision !== 'decline' && decision !== 'cancel')
810
- return fail(400, 'Choose an explicit decision for this request.');
811
- if (pending.kind !== 'questions' && decision === 'accept' && !authenticateApprover(who, body.get('password') ?? '').ok)
812
- return fail(403, 'Enter your password to approve this additional access.');
813
- const answers = body.has('answers') ? JSON.parse(body.get('answers')) : Object.fromEntries(pending.questions.map(q => [q.id, { answers: [body.get(`question:${q.id}`) ?? ''] }]));
814
- runtime.coding.answer(id, actor, token, decision, answers);
815
- }
816
- }
817
- // Authorization is checked again after asynchronous startup/transport work.
818
- if (!codingActorAllowed(actor))
819
- return fail(403, 'Your access changed. Sign in again.', 'unknown', id);
820
- return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, id })) : redirect(response, `/code/${id}`);
821
- }
822
- catch (error) {
823
- if (!codingActorAllowed(actor))
824
- return fail(403, 'Your access changed. Sign in again.', error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
825
- const message = error instanceof Error ? error.message : 'The coding action could not finish. Your draft is preserved.';
826
- const ship = /^\/code\/([a-f0-9]{32})\/ship$/.exec(url.pathname);
827
- if (ship && !wantsJson) {
828
- try {
829
- const preview = codingHandoffPreview(store, { sessionId: ship[1], actor: who.name });
830
- return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
831
- }
832
- catch { }
833
- }
834
- return fail(409, message, error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
835
- }
823
+ const token = body.get('requestId') ?? '';
824
+ const pending = runtime.coding.snapshot(id, actor).requests.find(r => r.id === token);
825
+ if (!pending)
826
+ return fail(409, 'This request is no longer waiting for an answer.');
827
+ const decision = body.get('decision');
828
+ if (decision !== 'accept' && decision !== 'decline' && decision !== 'cancel')
829
+ return fail(400, 'Choose an explicit decision for this request.');
830
+ if (pending.kind !== 'questions' && decision === 'accept' && !authenticateApprover(who, body.get('password') ?? '').ok)
831
+ return fail(403, 'Enter your password to approve this additional access.');
832
+ const answers = body.has('answers') ? JSON.parse(body.get('answers')) : Object.fromEntries(pending.questions.map(q => [q.id, { answers: [body.get(`question:${q.id}`) ?? ''] }]));
833
+ runtime.coding.answer(id, actor, token, decision, answers);
834
+ }
835
+ }
836
+ // Authorization is checked again after asynchronous startup/transport work.
837
+ if (!codingActorAllowed(actor))
838
+ return fail(403, 'Your access changed. Sign in again.', 'unknown', id);
839
+ return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, id })) : redirect(response, `/code/${id}`);
836
840
  }
837
- if (url.pathname === "/tasks/add") {
838
- const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
839
- const project = projectOf(who, request);
840
- if (project === undefined) {
841
- return refuse(response, who, 403, "that project is outside what this server was configured to show");
842
- }
843
- // Stale-tab guard: a form rendered under one open project must not
844
- // create into a different one switched-to since (finding 6).
845
- if (who.via === "cookie") {
846
- const seen = body.get("projectRevision");
847
- if (seen !== null && seen !== String(who.session.projectRevision)) {
848
- return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/tasks");
841
+ catch (error) {
842
+ if (!codingActorAllowed(actor))
843
+ return fail(403, 'Your access changed. Sign in again.', error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
844
+ const message = error instanceof Error ? error.message : 'The coding action could not finish. Your draft is preserved.';
845
+ if (verb === 'ship' && !wantsJson) {
846
+ try {
847
+ const preview = codingHandoffPreview(store, { sessionId: sessionId, actor: who.name });
848
+ return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
849
849
  }
850
+ catch { }
850
851
  }
851
- const id = (body.get("id") ?? "").trim();
852
- const title = body.get("title") ?? "";
853
- // The EFFECTIVE placement (repo onboarding, findings 15/35): the
854
- // trimmed, nonempty posted repo, else the open project — an empty
855
- // input falls through correctly. In root mode the effective path is
856
- // proved by authorizedProject EXACTLY as typed-then-canonicalized,
857
- // and [canonical] is the admitted list — a fresh clone under a root
858
- // must be able to receive its first task without waiting to appear
859
- // in any table.
860
- const repoGiven = (body.get("repo") ?? "").trim();
861
- const effective = repoGiven !== "" ? repoGiven : (project ?? "");
862
- if (restricted() && !visible(effective === "" ? null : effective))
863
- return refuse(response, who, 403, "That project is outside your access.", "/projects");
864
- // A scoped console refuses an EMPTY placement server-side (verification
865
- // finding 1): the form's `required` is a courtesy, not the guard — a
866
- // direct POST with no project open must not mint an unplaced task
867
- // under a ceiling. Unscoped mode keeps its historic unplaced filings.
868
- if (!unscopedMode && effective === "") {
869
- const csrf = who.via === "cookie" ? who.session.csrf : "";
870
- return sendScreen(response, 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, "name a repository — no project is open, so the task must say where it belongs", project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
871
- }
872
- let repo = effective;
873
- let admitted = unscopedMode ? null : admissionList() ?? [];
874
- const rootMode = !unscopedMode && ceiling.roots.length > 0;
875
- if (rootMode && effective !== "") {
876
- const canonical = (await authorizedProject(liveCeiling(), effective)) ? canonicalProject(effective) : null;
877
- if (canonical === null || canonical === undefined) {
878
- const csrf = who.via === "cookie" ? who.session.csrf : "";
879
- return sendScreen(response, 403, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, `${effective} is outside what this server was configured to show`, project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
880
- }
881
- repo = canonical;
882
- admitted = [canonical];
883
- }
884
- const goal = body.get("goal") ?? "";
885
- const notThis = body.get("not") ?? "";
886
- const touchesGiven = (body.get("touches") ?? "")
887
- .split(/[\n,]/)
888
- .map(one => one.trim())
889
- .filter(one => one !== "");
890
- const scout = body.get("scout") === "1";
891
- const permissionMode = body.get("permission-mode");
892
- if (permissionMode !== null && permissionMode !== "auto" && permissionMode !== "bypassPermissions") {
893
- return refuse(response, who, 400, "permissions must be Auto or Full access", "/tasks/new");
894
- }
895
- const qualityMode = body.get("quality-mode");
896
- if (qualityMode !== null && !isQualityMode(qualityMode)) {
897
- return refuse(response, who, 400, "quality must be Default or Strict / release", "/tasks/new");
898
- }
899
- // One filing door for every surface (Codex adoption review, finding 7).
900
- const after = (body.get("after") ?? "").trim();
901
- let chainProblem = null;
902
- const made = store.transact(() => {
903
- const filed = fileTaskProposal(store, {
904
- ...(id === "" ? {} : { id }),
905
- title,
906
- ...(repo === "" ? {} : { repo }),
907
- ...(goal === "" ? {} : { goal, acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")) }),
908
- outOfScope: notThis === "" ? null : notThis,
909
- touches: touchesGiven,
910
- ...(permissionMode === null ? {} : { permissionMode }),
911
- ...(qualityMode === null ? {} : { qualityMode }),
912
- ...(scout ? { deliverable: "report" } : {}),
913
- planning: scout
914
- ? "skip"
915
- : body.get("planning-policy") === "choice"
916
- ? body.get("plan-first") === "1" ? "required" : "skip"
917
- : "auto",
918
- filedVia: "console", filedBy: { name: who.name, kind: "person" },
919
- ...(admitted === null ? {} : { admittedRepos: admitted }),
920
- }, now);
921
- if (filed.ok && after !== "") {
922
- const afterRef = store.lookupRef(after);
923
- const chained = store.getTask(after) !== null && afterRef !== null && visible(afterRef.repo)
924
- ? store.addEdge(filed.id, after) : { ok: false, reason: "that task does not exist here" };
925
- if (!chained.ok)
926
- chainProblem = chained.reason;
927
- }
928
- // Missing dependencies leave a reviewable, inert task. They must
929
- // never be silently dropped before automatic approval starts work.
930
- if (filed.ok && chainProblem === null && who.via === "cookie")
931
- applyModeToNewFiling(store, filed.id, who.name, now);
932
- return filed;
933
- });
934
- if (!made.ok) {
935
- const csrf = who.via === "cookie" ? who.session.csrf : "";
936
- return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
937
- }
938
- const actionContext = requestContext.getStore();
939
- if (actionContext !== undefined)
940
- actionContext.createdTask = made.id;
941
- // A proved root-mode placement joins the project table (finding 15):
942
- // the new task's home is openable and admissible from now on.
943
- if (rootMode && repo !== "")
944
- store.upsertProject(repo, projectName(repo), now);
945
- if (chainProblem !== null)
946
- return taskScreen(response, who, made.id, `the task was created, but could not be made to wait for ${after} — ${chainProblem}`, 200);
947
- return redirect(response, taskHref(made.id));
948
- }
949
- if (url.pathname === "/queue/move" || url.pathname === "/queue/note") {
950
- const body = readForm(posted, CONSOLE_FORMS.queue);
951
- const project = projectOf(who, request);
952
- if (project === undefined) {
953
- return refuse(response, who, 403, "that project is outside what this server was configured to show");
954
- }
955
- // The note is a global runner label, not project-scoped work: the
956
- // fleet screen (project-less) posts it too, so the null-project gate
957
- // applies only to moves, which the task-level check below re-proves.
958
- if (url.pathname === "/queue/note") {
959
- const worker = (body.get("runner") ?? "").trim();
960
- const note = (body.get("note") ?? "").trim();
961
- if (note.length > 200 || hasForbiddenControls(note)) {
962
- return refuse(response, who, 400, "that note will not render, so it will not store", QUEUE_VIEW);
963
- }
964
- const set = store.setRunnerQueueNote(worker, note === "" ? null : note);
965
- if (!set.ok)
966
- return refuse(response, who, 404, "no such worker", QUEUE_VIEW);
967
- return redirect(response, body.get("from") === "fleet" ? "/fleet" : QUEUE_VIEW);
852
+ return fail(409, message, error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
853
+ }
854
+ };
855
+ async function addTask(ctx) {
856
+ const { who, request, response, now, posted } = ctx;
857
+ const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
858
+ const project = projectOf(who, request);
859
+ if (project === undefined) {
860
+ return refuse(response, who, 403, "that project is outside what this server was configured to show");
861
+ }
862
+ // Stale-tab guard: a form rendered under one open project must not
863
+ // create into a different one switched-to since (finding 6).
864
+ if (who.via === "cookie") {
865
+ const seen = body.get("projectRevision");
866
+ if (seen !== null && seen !== String(who.session.projectRevision)) {
867
+ return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/tasks");
968
868
  }
969
- if (who.via === "cookie") {
970
- const seen = body.get("projectRevision");
971
- if (seen !== null && seen !== String(who.session.projectRevision)) {
972
- return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", QUEUE_VIEW);
973
- }
869
+ }
870
+ const id = (body.get("id") ?? "").trim();
871
+ const title = body.get("title") ?? "";
872
+ // The EFFECTIVE placement (repo onboarding, findings 15/35): the
873
+ // trimmed, nonempty posted repo, else the open project — an empty
874
+ // input falls through correctly. In root mode the effective path is
875
+ // proved by authorizedProject EXACTLY as typed-then-canonicalized,
876
+ // and [canonical] is the admitted list — a fresh clone under a root
877
+ // must be able to receive its first task without waiting to appear
878
+ // in any table.
879
+ const repoGiven = (body.get("repo") ?? "").trim();
880
+ const effective = repoGiven !== "" ? repoGiven : (project ?? "");
881
+ if (restricted() && !visible(effective === "" ? null : effective))
882
+ return refuse(response, who, 403, "That project is outside your access.", "/projects");
883
+ // A scoped console refuses an EMPTY placement server-side (verification
884
+ // finding 1): the form's `required` is a courtesy, not the guard — a
885
+ // direct POST with no project open must not mint an unplaced task
886
+ // under a ceiling. Unscoped mode keeps its historic unplaced filings.
887
+ if (!unscopedMode && effective === "") {
888
+ return sendScreen(response, 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, "name a repository — no project is open, so the task must say where it belongs", project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
889
+ }
890
+ let repo = effective;
891
+ let admitted = unscopedMode ? null : admissionList() ?? [];
892
+ const rootMode = !unscopedMode && ceiling.roots.length > 0;
893
+ if (rootMode && effective !== "") {
894
+ const canonical = (await authorizedProject(liveCeiling(), effective)) ? canonicalProject(effective) : null;
895
+ if (canonical === null || canonical === undefined) {
896
+ return sendScreen(response, 403, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, `${effective} is outside what this server was configured to show`, project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
897
+ }
898
+ repo = canonical;
899
+ admitted = [canonical];
900
+ }
901
+ const goal = body.get("goal") ?? "";
902
+ const notThis = body.get("not") ?? "";
903
+ const touchesGiven = (body.get("touches") ?? "")
904
+ .split(/[\n,]/)
905
+ .map(one => one.trim())
906
+ .filter(one => one !== "");
907
+ const scout = body.get("scout") === "1";
908
+ const permissionMode = body.get("permission-mode");
909
+ if (permissionMode !== null && permissionMode !== "auto" && permissionMode !== "bypassPermissions") {
910
+ return refuse(response, who, 400, "permissions must be Auto or Full access", "/tasks/new");
911
+ }
912
+ const qualityMode = body.get("quality-mode");
913
+ if (qualityMode !== null && !isQualityMode(qualityMode)) {
914
+ return refuse(response, who, 400, "quality must be Default or Strict / release", "/tasks/new");
915
+ }
916
+ // One filing door for every surface (Codex adoption review, finding 7).
917
+ const after = (body.get("after") ?? "").trim();
918
+ let chainProblem = null;
919
+ const made = store.transact(() => {
920
+ const filed = fileTaskProposal(store, {
921
+ ...(id === "" ? {} : { id }),
922
+ title,
923
+ ...(repo === "" ? {} : { repo }),
924
+ ...(goal === "" ? {} : { goal, acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")) }),
925
+ outOfScope: notThis === "" ? null : notThis,
926
+ touches: touchesGiven,
927
+ ...(permissionMode === null ? {} : { permissionMode }),
928
+ ...(qualityMode === null ? {} : { qualityMode }),
929
+ ...(scout ? { deliverable: "report" } : {}),
930
+ planning: scout
931
+ ? "skip"
932
+ : body.get("planning-policy") === "choice"
933
+ ? body.get("plan-first") === "1" ? "required" : "skip"
934
+ : "auto",
935
+ filedVia: "console", filedBy: { name: who.name, kind: "person" },
936
+ ...(admitted === null ? {} : { admittedRepos: admitted }),
937
+ }, now);
938
+ if (filed.ok && after !== "") {
939
+ const afterRef = store.lookupRef(after);
940
+ const chained = store.getTask(after) !== null && afterRef !== null && visible(afterRef.repo)
941
+ ? store.addEdge(filed.id, after) : { ok: false, reason: "that task does not exist here" };
942
+ if (!chained.ok)
943
+ chainProblem = chained.reason;
944
+ }
945
+ // Missing dependencies leave a reviewable, inert task. They must
946
+ // never be silently dropped before automatic approval starts work.
947
+ if (filed.ok && chainProblem === null && who.via === "cookie")
948
+ applyModeToNewFiling(store, filed.id, who.name, now);
949
+ return filed;
950
+ });
951
+ if (!made.ok) {
952
+ return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
953
+ }
954
+ const actionContext = requestContext.getStore();
955
+ if (actionContext !== undefined)
956
+ actionContext.createdTask = made.id;
957
+ // A proved root-mode placement joins the project table (finding 15):
958
+ // the new task's home is openable and admissible from now on.
959
+ if (rootMode && repo !== "")
960
+ store.upsertProject(repo, projectName(repo), now);
961
+ if (chainProblem !== null)
962
+ return taskScreen(response, who, made.id, `the task was created, but could not be made to wait for ${after} — ${chainProblem}`, 200);
963
+ return redirect(response, taskHref(made.id));
964
+ }
965
+ async function queueNote(ctx) {
966
+ const { who, request, response, posted } = ctx;
967
+ const body = readForm(posted, CONSOLE_FORMS.queue);
968
+ const project = projectOf(who, request);
969
+ if (project === undefined) {
970
+ return refuse(response, who, 403, "that project is outside what this server was configured to show");
971
+ }
972
+ // The note is a global runner label, not project-scoped work: the
973
+ // fleet screen (project-less) posts it too, so the null-project gate
974
+ // applies only to moves, which their task-level check re-proves.
975
+ const worker = (body.get("runner") ?? "").trim();
976
+ const note = (body.get("note") ?? "").trim();
977
+ if (note.length > 200 || hasForbiddenControls(note)) {
978
+ return refuse(response, who, 400, "that note will not render, so it will not store", QUEUE_VIEW);
979
+ }
980
+ const set = store.setRunnerQueueNote(worker, note === "" ? null : note);
981
+ if (!set.ok)
982
+ return refuse(response, who, 404, "no such worker", QUEUE_VIEW);
983
+ return redirect(response, body.get("from") === "fleet" ? "/fleet" : QUEUE_VIEW);
984
+ }
985
+ async function queueMove(ctx) {
986
+ const { who, request, response, posted } = ctx;
987
+ const body = readForm(posted, CONSOLE_FORMS.queue);
988
+ const project = projectOf(who, request);
989
+ if (project === undefined) {
990
+ return refuse(response, who, 403, "that project is outside what this server was configured to show");
991
+ }
992
+ if (who.via === "cookie") {
993
+ const seen = body.get("projectRevision");
994
+ if (seen !== null && seen !== String(who.session.projectRevision)) {
995
+ return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", QUEUE_VIEW);
974
996
  }
975
- // A drag posts in place (fetch) when it can; the page re-renders its
976
- // own fragment on success. A plain form still works with no script.
977
- const inPlace = body.get("respond") === "fragment";
978
- const fromFleet = who.via === "cookie" && body.get("projectRevision") === null;
979
- const respondMove = (status, message) => {
980
- if (!inPlace)
981
- return status === 409 ? refuse(response, who, 409, message, QUEUE_VIEW) : redirect(response, QUEUE_VIEW);
982
- respond(response, status, "text/plain; charset=utf-8", message);
983
- };
984
- const moveReason = (reason) => reason === "stale"
985
- ? "the queue moved underneath you — it just reloaded"
986
- : reason === "claimed"
987
- ? "that task is being taken right now — it keeps its claim"
988
- : reason === "worker-retired"
989
- ? "that worker is retired — drag its work elsewhere, or register the name again"
990
- : reason === "no-such-worker"
991
- ? "no such worker"
992
- : "that task is not in this queue any more";
993
- const taskId = (body.get("task") ?? "").trim();
994
- const columnGiven = (body.get("column") ?? "").trim();
995
- const toRunner = columnGiven === "" || columnGiven === "anyone" ? null : columnGiven;
996
- const beforeGiven = (body.get("before") ?? "").trim();
997
- const revisionGiven = Number(body.get("queueRevision") ?? "");
998
- // The queue's own screen enforces the open project; the fleet screen
999
- // is cross-project, so the ceiling is the only wall it needs.
1000
- const belongs = (id) => {
1001
- const ref = store.lookupRef(id);
1002
- return ref !== null && visible(ref.repo) && (fromFleet || project === null || ref.repo === null || ref.repo === project);
1003
- };
1004
- if (!belongs(taskId) || (beforeGiven !== "" && beforeGiven !== QUEUE_FRONT && !belongs(beforeGiven))) {
997
+ }
998
+ // A drag posts in place (fetch) when it can; the page re-renders its
999
+ // own fragment on success. A plain form still works with no script.
1000
+ const inPlace = body.get("respond") === "fragment";
1001
+ const fromFleet = who.via === "cookie" && body.get("projectRevision") === null;
1002
+ const respondMove = (status, message) => {
1003
+ if (!inPlace)
1004
+ return status === 409 ? refuse(response, who, 409, message, QUEUE_VIEW) : redirect(response, QUEUE_VIEW);
1005
+ respond(response, status, "text/plain; charset=utf-8", message);
1006
+ };
1007
+ const moveReason = (reason) => reason === "stale"
1008
+ ? "the queue moved underneath you — it just reloaded"
1009
+ : reason === "claimed"
1010
+ ? "that task is being taken right now — it keeps its claim"
1011
+ : reason === "worker-retired"
1012
+ ? "that worker is retired — drag its work elsewhere, or register the name again"
1013
+ : reason === "no-such-worker"
1014
+ ? "no such worker"
1015
+ : "that task is not in this queue any more";
1016
+ const taskId = (body.get("task") ?? "").trim();
1017
+ const columnGiven = (body.get("column") ?? "").trim();
1018
+ const toRunner = columnGiven === "" || columnGiven === "anyone" ? null : columnGiven;
1019
+ const beforeGiven = (body.get("before") ?? "").trim();
1020
+ const revisionGiven = Number(body.get("queueRevision") ?? "");
1021
+ // The queue's own screen enforces the open project; the fleet screen
1022
+ // is cross-project, so the ceiling is the only wall it needs.
1023
+ const belongs = (id) => {
1024
+ const ref = store.lookupRef(id);
1025
+ return ref !== null && visible(ref.repo) && (fromFleet || project === null || ref.repo === null || ref.repo === project);
1026
+ };
1027
+ if (!belongs(taskId) || (beforeGiven !== "" && beforeGiven !== QUEUE_FRONT && !belongs(beforeGiven))) {
1028
+ return respondMove(404, "that task is not in this queue");
1029
+ }
1030
+ // The no-script "move to the front" button cannot name the front: the
1031
+ // front of a task's partition is decided by the store's exact repo AND
1032
+ // assignment, and the page's snapshot is bounded — so the sentinel is
1033
+ // resolved HERE, against a fresh snapshot, into a real task id (slice
1034
+ // 1b, fix 1). It is honored only within the task's own column; a
1035
+ // cross-column front is not provable from a bounded snapshot.
1036
+ let beforeTaskId = beforeGiven === "" ? null : beforeGiven;
1037
+ if (beforeGiven === QUEUE_FRONT) {
1038
+ const ref = store.lookupRef(taskId);
1039
+ if (ref === null)
1005
1040
  return respondMove(404, "that task is not in this queue");
1006
- }
1007
- // The no-script "move to the front" button cannot name the front: the
1008
- // front of a task's partition is decided by the store's exact repo AND
1009
- // assignment, and the page's snapshot is bounded — so the sentinel is
1010
- // resolved HERE, against a fresh snapshot, into a real task id (slice
1011
- // 1b, fix 1). It is honored only within the task's own column; a
1012
- // cross-column front is not provable from a bounded snapshot.
1013
- let beforeTaskId = beforeGiven === "" ? null : beforeGiven;
1014
- if (beforeGiven === QUEUE_FRONT) {
1015
- const ref = store.lookupRef(taskId);
1016
- if (ref === null)
1017
- return respondMove(404, "that task is not in this queue");
1018
- if (ref.assignedRunner !== toRunner) {
1019
- return respondMove(409, "move to the front works inside a task's own column — drag it across to reserve it elsewhere");
1020
- }
1021
- const now = clock();
1022
- const snapshot = store.queueScoped(ref.repo, now);
1023
- const self = snapshot.find(one => one.id === taskId);
1024
- // A claim can land after the form rendered WITHOUT
1025
- // bumping queueRevision, so the snapshot — not the form — decides
1026
- // whether the card is still free. A taken or vanished card is the
1027
- // typed refusal, never a silent no-op that would skip moveTask()'s
1028
- // own claimed recheck.
1029
- if (self === undefined)
1030
- return respondMove(409, moveReason("unknown-task"));
1031
- if (self.taken)
1032
- return respondMove(409, moveReason("claimed"));
1033
- const partition = snapshot.filter(one => one.repo === ref.repo && one.assignedRunner === ref.assignedRunner && !one.taken);
1034
- const front = partition[0];
1035
- if (front === undefined)
1036
- return respondMove(409, moveReason("unknown-task"));
1037
- if (front.id === taskId) {
1038
- // Already the front: a no-op, but only against the revision the
1039
- // form was rendered with — this branch never reaches moveTask()'s
1040
- // CAS, so the check is made here.
1041
- if (!Number.isSafeInteger(revisionGiven) || revisionGiven !== store.queueRevision()) {
1042
- return respondMove(409, moveReason("stale"));
1043
- }
1044
- return respondMove(200, "already at the front");
1045
- }
1046
- beforeTaskId = front.id;
1047
- }
1048
- const moved = store.moveTask({
1049
- taskId,
1050
- toRunner,
1051
- beforeTaskId,
1052
- ...(Number.isSafeInteger(revisionGiven) ? { queueRevision: revisionGiven } : {}),
1053
- }, clock());
1054
- if (!moved.ok) {
1055
- return respondMove(409, moveReason(moved.reason));
1056
- }
1057
- return respondMove(200, "moved");
1041
+ if (ref.assignedRunner !== toRunner) {
1042
+ return respondMove(409, "move to the front works inside a task's own column — drag it across to reserve it elsewhere");
1043
+ }
1044
+ const now = clock();
1045
+ const snapshot = store.queueScoped(ref.repo, now);
1046
+ const self = snapshot.find(one => one.id === taskId);
1047
+ // A claim can land after the form rendered WITHOUT
1048
+ // bumping queueRevision, so the snapshot — not the form — decides
1049
+ // whether the card is still free. A taken or vanished card is the
1050
+ // typed refusal, never a silent no-op that would skip moveTask()'s
1051
+ // own claimed recheck.
1052
+ if (self === undefined)
1053
+ return respondMove(409, moveReason("unknown-task"));
1054
+ if (self.taken)
1055
+ return respondMove(409, moveReason("claimed"));
1056
+ const partition = snapshot.filter(one => one.repo === ref.repo && one.assignedRunner === ref.assignedRunner && !one.taken);
1057
+ const front = partition[0];
1058
+ if (front === undefined)
1059
+ return respondMove(409, moveReason("unknown-task"));
1060
+ if (front.id === taskId) {
1061
+ // Already the front: a no-op, but only against the revision the
1062
+ // form was rendered with — this branch never reaches moveTask()'s
1063
+ // CAS, so the check is made here.
1064
+ if (!Number.isSafeInteger(revisionGiven) || revisionGiven !== store.queueRevision()) {
1065
+ return respondMove(409, moveReason("stale"));
1066
+ }
1067
+ return respondMove(200, "already at the front");
1068
+ }
1069
+ beforeTaskId = front.id;
1070
+ }
1071
+ const moved = store.moveTask({
1072
+ taskId,
1073
+ toRunner,
1074
+ beforeTaskId,
1075
+ ...(Number.isSafeInteger(revisionGiven) ? { queueRevision: revisionGiven } : {}),
1076
+ }, clock());
1077
+ if (!moved.ok) {
1078
+ return respondMove(409, moveReason(moved.reason));
1058
1079
  }
1080
+ return respondMove(200, "moved");
1081
+ }
1082
+ async function answerDecision(ctx) {
1083
+ const { url, who, response, now, posted } = ctx;
1059
1084
  const answer = /^\/d\/([0-9]{1,15})\/answer$/.exec(url.pathname);
1060
- if (answer !== null) {
1061
- const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
1062
- const id = Number(answer[1]);
1063
- const requestedReturn = body.get("return");
1064
- const decisionBack = requestedReturn === "next"
1065
- ? "/next"
1066
- : requestedReturn === null
1067
- ? `/d/${id}`
1068
- : safeChatReturn(requestedReturn);
1069
- const decision = store.getDecision(id);
1070
- if (decision === null)
1071
- return refuse(response, who, 404, "no such decision");
1072
- const answeringRun = store.getRun(decision.run);
1073
- if (answeringRun !== null && !visible(taskRepoOf(answeringRun.taskRef))) {
1074
- return refuse(response, who, 404, "no such decision");
1075
- }
1076
- const choice = body.get("choice") ?? "";
1077
- const chosen = decision.options.find(option => option.id === choice);
1078
- // Irreversible options never ride one accidental tap: the form arms
1079
- // them behind an explicit confirmation field, and the server checks —
1080
- // the client rendering is convenience, this is the rule.
1081
- if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
1082
- return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
1083
- }
1084
- const note = body.get("note");
1085
- const answered = store.answerDecision({
1086
- id,
1087
- choice,
1088
- by: who.name,
1089
- via: "web",
1090
- ...(note === null || note === "" ? {} : { note }),
1091
- }, now);
1092
- if (!answered.ok) {
1093
- const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
1094
- const why = answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1095
- return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
1096
- }
1097
- return redirect(response, decisionBack);
1098
- }
1099
- const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
1100
- if (act !== null && act.verb === "confirm-stopped") {
1101
- const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
1102
- // Confirming a finished build stopped is an approver's act behind the password, typed again — the same
1103
- // record as `toolroll run settle`, refused while anything of the run may still be running.
1104
- const back = taskHref(act.taskId);
1105
- if (who.via !== "cookie" || who.role !== "approver")
1106
- return refuse(response, who, 403, "Only an approver can confirm a build stopped.", back);
1107
- const ref = store.lookupRef(act.taskId);
1108
- if (ref === null || !visible(ref.repo))
1109
- return refuse(response, who, 404, "no such task", "/tasks");
1110
- const named = (body.get("run") ?? "").trim();
1111
- const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1112
- const runTask = run === null ? null : store.externalIdFor(run.taskRef);
1113
- if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
1114
- return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
1115
- }
1116
- if (!authenticateApprover(who, body.get("token") ?? "", ref.repo).ok || !store.accountCanAccess(who.name, ref.repo)) {
1117
- return taskScreen(response, who, act.taskId, "That password didn't match. Nothing changed.", 403);
1118
- }
1119
- // When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
1120
- if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
1121
- return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
1122
- }
1123
- const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
1124
- if (!settled.ok) {
1125
- return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
1126
- ? `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);
1127
- }
1128
- bustBadge();
1129
- const to = resultReturnTarget(body.get("return"), run.id);
1130
- return redirect(response, body.get("return") ? to : back);
1131
- }
1132
- if (act !== null && act.verb === "merge") {
1133
- const body = readForm(posted, CONSOLE_FORMS.merge);
1134
- // Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
1135
- // result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
1136
- if (who.via !== "cookie" || who.role !== "approver")
1137
- return refuse(response, who, 403, "Only an approver can merge.");
1138
- if (store.isDemo())
1139
- return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
1140
- const ref = store.lookupRef(act.taskId);
1141
- if (ref === null || !visible(ref.repo))
1142
- return refuse(response, who, 404, "no such task", "/tasks");
1143
- const named = (body.get("run") ?? "").trim();
1144
- const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1145
- const runTask = run === null ? null : store.externalIdFor(run.taskRef);
1146
- if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
1147
- return taskScreen(response, who, act.taskId, "That pull request isn't part of this task.", 409);
1148
- }
1149
- const merged = await mergeAsPerson(store, { runId: run.id, name: who.name, password: body.get("token") ?? "", ...(body.get("anyway") === "1" ? { anyway: true } : {}), ...(options.publishExec === undefined ? {} : { exec: options.publishExec }), clock });
1150
- if (!merged.ok)
1151
- return taskScreen(response, who, act.taskId, merged.message, merged.reason === "password" ? 403 : 409);
1152
- return redirect(response, `${taskHref(act.taskId)}#merge`);
1153
- }
1154
- if (act !== null) {
1155
- return taskMutation(response, who, act.taskId, act.verb, posted, now);
1156
- }
1157
- const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
1158
- if (runNote !== null) {
1159
- const body = readForm(posted, CONSOLE_FORMS.runNote);
1160
- // An operator's verdict beside the machine's record (M6): immutable,
1161
- // bounded by the same validator as decision notes, ceiling-checked
1162
- // like every run resource. Ordinary authenticated mutation — no
1163
- // nonce, because nothing here approves anything.
1164
- const id = Number(runNote[1]);
1165
- const found = store.getRun(id);
1166
- if (found === null || !visible(taskRepoOf(found.taskRef))) {
1167
- return refuse(response, who, 404, "no such run");
1168
- }
1169
- const note = validateNote(body.get("note") ?? "");
1170
- if (!note.ok)
1171
- return refuse(response, who, 400, note.problem);
1172
- store.addRunNote(id, who.name, note.note, now);
1173
- return redirect(response, `/r/${id}`);
1085
+ if (answer === null)
1086
+ return noPage(who, response);
1087
+ const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
1088
+ const id = Number(answer[1]);
1089
+ const requestedReturn = body.get("return");
1090
+ const decisionBack = requestedReturn === "next"
1091
+ ? "/next"
1092
+ : requestedReturn === null
1093
+ ? `/d/${id}`
1094
+ : safeChatReturn(requestedReturn);
1095
+ const decision = store.getDecision(id);
1096
+ if (decision === null)
1097
+ return refuse(response, who, 404, "no such decision");
1098
+ const answeringRun = store.getRun(decision.run);
1099
+ if (answeringRun !== null && !visible(taskRepoOf(answeringRun.taskRef))) {
1100
+ return refuse(response, who, 404, "no such decision");
1174
1101
  }
1102
+ const choice = body.get("choice") ?? "";
1103
+ const chosen = decision.options.find(option => option.id === choice);
1104
+ // Irreversible options never ride one accidental tap: the form arms
1105
+ // them behind an explicit confirmation field, and the server checks —
1106
+ // the client rendering is convenience, this is the rule.
1107
+ if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
1108
+ return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
1109
+ }
1110
+ const note = body.get("note");
1111
+ const answered = store.answerDecision({
1112
+ id,
1113
+ choice,
1114
+ by: who.name,
1115
+ via: "web",
1116
+ ...(note === null || note === "" ? {} : { note }),
1117
+ }, now);
1118
+ if (!answered.ok) {
1119
+ const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
1120
+ const why = answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1121
+ return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
1122
+ }
1123
+ return redirect(response, decisionBack);
1124
+ }
1125
+ async function taskConfirmStopped(ctx) {
1126
+ const { url, who, response, now, posted } = ctx;
1127
+ const act = matchTaskPath(url.pathname, "/confirm-stopped$");
1128
+ if (act === null)
1129
+ return noPage(who, response);
1130
+ const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
1131
+ // Confirming a finished build stopped is an approver's act behind the password, typed again — the same
1132
+ // record as `toolroll run settle`, refused while anything of the run may still be running.
1133
+ const back = taskHref(act.taskId);
1134
+ const ref = store.lookupRef(act.taskId);
1135
+ if (ref === null || !visible(ref.repo))
1136
+ return refuse(response, who, 404, "no such task", "/tasks");
1137
+ const named = (body.get("run") ?? "").trim();
1138
+ const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1139
+ const runTask = run === null ? null : store.externalIdFor(run.taskRef);
1140
+ if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
1141
+ return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
1142
+ }
1143
+ if (!authenticateApprover(who, body.get("token") ?? "", ref.repo).ok || !store.accountCanAccess(who.name, ref.repo)) {
1144
+ return taskScreen(response, who, act.taskId, "That password didn't match. Nothing changed.", 403);
1145
+ }
1146
+ // When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
1147
+ if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
1148
+ return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
1149
+ }
1150
+ const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
1151
+ if (!settled.ok) {
1152
+ return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
1153
+ ? `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);
1154
+ }
1155
+ bustBadge();
1156
+ const to = resultReturnTarget(body.get("return"), run.id);
1157
+ return redirect(response, body.get("return") ? to : back);
1158
+ }
1159
+ async function taskMerge(ctx) {
1160
+ const { url, who, response, posted } = ctx;
1161
+ const act = matchTaskPath(url.pathname, "/merge$");
1162
+ if (act === null)
1163
+ return noPage(who, response);
1164
+ const body = readForm(posted, CONSOLE_FORMS.merge);
1165
+ // Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
1166
+ // result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
1167
+ if (store.isDemo())
1168
+ return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
1169
+ const ref = store.lookupRef(act.taskId);
1170
+ if (ref === null || !visible(ref.repo))
1171
+ return refuse(response, who, 404, "no such task", "/tasks");
1172
+ const named = (body.get("run") ?? "").trim();
1173
+ const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1174
+ const runTask = run === null ? null : store.externalIdFor(run.taskRef);
1175
+ if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
1176
+ return taskScreen(response, who, act.taskId, "That pull request isn't part of this task.", 409);
1177
+ }
1178
+ const merged = await mergeAsPerson(store, { runId: run.id, name: who.name, password: body.get("token") ?? "", ...(body.get("anyway") === "1" ? { anyway: true } : {}), ...(options.publishExec === undefined ? {} : { exec: options.publishExec }), clock });
1179
+ if (!merged.ok)
1180
+ return taskScreen(response, who, act.taskId, merged.message, merged.reason === "password" ? 403 : 409);
1181
+ return redirect(response, `${taskHref(act.taskId)}#merge`);
1182
+ }
1183
+ /** A task act bound to its verb: taskMutation owns each verb's checks. */
1184
+ const taskAct = (verb) => async (ctx) => {
1185
+ const { url, who, response, now, posted } = ctx;
1186
+ const act = matchTaskPath(url.pathname, `/${verb}$`);
1187
+ if (act === null)
1188
+ return noPage(who, response);
1189
+ return taskMutation(response, who, act.taskId, verb, posted, now);
1190
+ };
1191
+ async function runNote(ctx) {
1192
+ const { url, who, response, now, posted } = ctx;
1193
+ const noted = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
1194
+ if (noted === null)
1195
+ return noPage(who, response);
1196
+ const body = readForm(posted, CONSOLE_FORMS.runNote);
1197
+ // An operator's verdict beside the machine's record (M6): immutable,
1198
+ // bounded by the same validator as decision notes, ceiling-checked
1199
+ // like every run resource. Ordinary authenticated mutation — no
1200
+ // nonce, because nothing here approves anything.
1201
+ const id = Number(noted[1]);
1202
+ const found = store.getRun(id);
1203
+ if (found === null || !visible(taskRepoOf(found.taskRef))) {
1204
+ return refuse(response, who, 404, "no such run");
1205
+ }
1206
+ const note = validateNote(body.get("note") ?? "");
1207
+ if (!note.ok)
1208
+ return refuse(response, who, 400, note.problem);
1209
+ store.addRunNote(id, who.name, note.note, now);
1210
+ return redirect(response, `/r/${id}`);
1211
+ }
1212
+ async function runComment(ctx) {
1213
+ const { url, who, response, now, posted } = ctx;
1175
1214
  const diffComment = /^\/r\/([0-9]{1,15})\/comment$/.exec(url.pathname);
1176
- if (diffComment !== null) {
1177
- const body = readForm(posted, CONSOLE_FORMS.diffComment);
1178
- // A review comment on the IMMUTABLE terminal diff (M6.8): bound to
1179
- // the exact artifact and its hash. Ordinary authenticated mutation —
1180
- // the nonce belongs to the approval screen that later restates the
1181
- // batch, never to the comment box.
1182
- const id = Number(diffComment[1]);
1183
- const found = store.getRun(id);
1184
- if (found === null || !visible(taskRepoOf(found.taskRef))) {
1185
- return refuse(response, who, 404, "no such run");
1186
- }
1187
- // Where the reader came from (package 3): the cockpit's deep link,
1188
- // the chat's result view, or the run page — validated to those exact
1189
- // shapes. A refusal sends them back THERE, where the browser has kept
1190
- // their draft, so a failed submission is recoverable in place.
1191
- const returnTo = resultReturnTarget(body.get("return"), id);
1192
- const selectedTab = parseResultTab(body.get("tab"));
1193
- const back = selectedTab === "summary" ? returnTo : `${returnTo}${returnTo.includes("?") ? "&" : "?"}tab=${selectedTab}`;
1194
- if (body.get("intent") === "revise") {
1195
- const result = requestResultChanges(store, evidenceRoot, {
1196
- run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1197
- repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
1198
- note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"),
1199
- }, now);
1200
- if (!result.ok) {
1201
- const request = body.get("request") ?? "";
1202
- const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : back;
1203
- return refuse(response, who, result.status, result.message, retryBack);
1204
- }
1205
- return redirect(response, revisionDestination(result.id, returnTo));
1206
- }
1207
- const terminal = store.artifactsFor(id).find(one => one.kind === "terminal-diff");
1208
- if (terminal === undefined) {
1209
- return refuse(response, who, 400, "this run has no terminal diff to comment on", back);
1210
- }
1211
- // The bytes must VERIFY before words attach to them (audit IV-10):
1212
- // "a comment on the exact reviewed bytes" is a lie if the bytes are
1213
- // gone or no longer hash to their record.
1214
- const proven = readVerifiedArtifact(evidenceRoot, terminal);
1215
- if (!proven.ok) {
1216
- return refuse(response, who, 409, `the terminal diff no longer verifies (${proven.problem}) — nothing to comment on`, back);
1217
- }
1218
- const note = validateNote(body.get("note") ?? "");
1219
- if (!note.ok)
1220
- return refuse(response, who, 400, note.problem, back);
1221
- const rawPath = (body.get("path") ?? "").trim();
1222
- if (rawPath.length > 300 || hasForbiddenControls(rawPath)) {
1223
- return refuse(response, who, 400, "Enter a file path of up to 300 characters, without control characters.", back);
1224
- }
1225
- const rawLine = (body.get("line") ?? "").trim();
1226
- const line = rawLine === "" ? null : Number(rawLine);
1227
- if (line !== null && (!Number.isInteger(line) || line < 1 || line > 1_000_000)) {
1228
- return refuse(response, who, 400, "Enter a whole line number from 1 to 1,000,000, or leave it blank.", back);
1229
- }
1230
- // The form's own request token (package 3) is the dedupe key: a
1231
- // replayed or double submission finds its note already recorded and
1232
- // lands on the same receipt instead of a second note. The token is
1233
- // bound to the account, so nobody else's replay can consume it.
1234
- const request = body.get("request");
1235
- const sourceKey = commentSourceKey(who.name, request);
1236
- const path = rawPath === "" ? null : rawPath;
1237
- const inserted = store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, ...(sourceKey === undefined ? {} : { sourceKey }) }, now);
1238
- if (inserted === null && sourceKey !== undefined) {
1239
- // The token was used before (repair 2026-09-14, finding 1). A
1240
- // request identity is IMMUTABLE: it names exactly one note on
1241
- // exactly one result. The unchanged retry — same run, same file,
1242
- // line and words — lands on its original receipt; any other reuse
1243
- // is refused, and the refusal's way back carries `conflict=<token>`
1244
- // so the browser mints a fresh identity for the words it still
1245
- // holds. Nothing typed is lost either way.
1246
- const earlier = store.diffCommentBySourceKey(sourceKey);
1247
- const conflictBack = `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes`;
1248
- if (earlier === null || earlier.run !== id) {
1249
- return refuse(response, who, 409, `this note's request identity was already used on another result (build #${earlier === null ? "?" : earlier.run}) — go back; your words are kept, and the form will carry a new identity`, conflictBack);
1250
- }
1251
- if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note) {
1252
- return refuse(response, who, 409, `this request identity already recorded a different note on this result (${earlier.path === null ? "no file" : `${earlier.path}${earlier.line === null ? "" : `:${earlier.line}`}`}: "${oneLineOf(earlier.note, 80)}") — go back; your edited words are kept as a new note, and the form will carry a new identity`, conflictBack);
1253
- }
1254
- }
1255
- // Land back AT the request-changes form with the note field ready —
1256
- // writing five notes in a row must cost five keystrokes of
1257
- // navigation, not five scrolls (arc 6, finding 5/6). The receipt
1258
- // names the request token, so the browser clears exactly the draft
1259
- // that landed and no other.
1260
- const receipt = sourceKey === undefined ? "1" : request;
1261
- return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
1262
- }
1263
- if (url.pathname === "/session/editor-links") {
1264
- const body = readForm(posted, CONSOLE_FORMS.editorLinks);
1265
- // The session half of the editor-link activation (arc 6, finding 1):
1266
- // only the person at the browser can say "this device holds the
1267
- // worktrees". Per-session, dies with the session, grants nothing —
1268
- // it only lets already-authorized pages RENDER vscode links.
1269
- if (who.via !== "cookie")
1270
- return refuse(response, who, 403, "editor links are a browser session's choice");
1271
- if (options.editorLinks === undefined)
1272
- return refuse(response, who, 404, "editor links are not enabled on this server");
1273
- who.session.editorLinks = body.get("on") === "1";
1274
- const back = body.get("return") ?? "/";
1275
- return redirect(response, /^\/[a-z0-9/_-]*$/i.test(back) ? back : "/");
1215
+ if (diffComment === null)
1216
+ return noPage(who, response);
1217
+ const body = readForm(posted, CONSOLE_FORMS.diffComment);
1218
+ // A review comment on the IMMUTABLE terminal diff (M6.8): bound to
1219
+ // the exact artifact and its hash. Ordinary authenticated mutation —
1220
+ // the nonce belongs to the approval screen that later restates the
1221
+ // batch, never to the comment box.
1222
+ const id = Number(diffComment[1]);
1223
+ const found = store.getRun(id);
1224
+ if (found === null || !visible(taskRepoOf(found.taskRef))) {
1225
+ return refuse(response, who, 404, "no such run");
1276
1226
  }
1277
- const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
1278
- if (revise !== null) {
1279
- const body = readForm(posted, CONSOLE_FORMS.revise);
1280
- // Seal the live comment batch into ONE unapproved revision task with
1281
- // an immutable brief (M6.8). Deterministic — no model reads anything
1282
- // here — and every revision takes its own approval: comments can
1283
- // semantically widen work, and no path check can prove they did not.
1284
- const id = Number(revise[1]);
1285
- const found = store.getRun(id);
1286
- if (found === null || !visible(taskRepoOf(found.taskRef))) {
1287
- return refuse(response, who, 404, "no such run");
1288
- }
1289
- const back = resultReturnTarget(body.get("return"), id);
1290
- const result = createResultRevision(store, evidenceRoot, {
1227
+ // Where the reader came from (package 3): the cockpit's deep link,
1228
+ // the chat's result view, or the run page — validated to those exact
1229
+ // shapes. A refusal sends them back THERE, where the browser has kept
1230
+ // their draft, so a failed submission is recoverable in place.
1231
+ const returnTo = resultReturnTarget(body.get("return"), id);
1232
+ const selectedTab = parseResultTab(body.get("tab"));
1233
+ const back = selectedTab === "summary" ? returnTo : `${returnTo}${returnTo.includes("?") ? "&" : "?"}tab=${selectedTab}`;
1234
+ if (body.get("intent") === "revise") {
1235
+ const result = requestResultChanges(store, evidenceRoot, {
1291
1236
  run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1292
1237
  repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
1238
+ note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"),
1293
1239
  }, now);
1294
- if (!result.ok)
1295
- return refuse(response, who, result.status, result.message, back);
1296
- return redirect(response, revisionDestination(result.id, back));
1297
- }
1298
- // Follow-ups on a result: Run checks on its exact commit (a worker runs the
1299
- // approved command once and seals the log), or file a task to add tests.
1300
- const followUp = /^\/r\/([0-9]{1,15})\/(checks|add-tests)$/.exec(url.pathname);
1301
- if (followUp !== null) {
1302
- const body = readForm(posted, CONSOLE_FORMS.followUp);
1303
- const id = Number(followUp[1]);
1304
- const found = store.getRun(id);
1305
- if (found === null || !visible(taskRepoOf(found.taskRef)))
1306
- return refuse(response, who, 404, "no such run");
1307
- // The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
1308
- const runTask = store.externalIdFor(found.taskRef);
1309
- const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
1310
- if (who.via !== "cookie")
1311
- return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
1312
- if (followUp[2] === "checks") {
1313
- if (who.role !== "approver")
1314
- return refuse(response, who, 403, "An approver runs checks.", back);
1315
- if (store.isDemo())
1316
- return refuse(response, who, 403, "The demo runs no checks.", back);
1317
- const level = body.get("level") === "full" ? "full" : "quick";
1318
- const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
1319
- if (!asked.ok)
1320
- return refuse(response, who, 409, asked.message, back);
1321
- // Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
1322
- return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
1240
+ if (!result.ok) {
1241
+ const request = body.get("request") ?? "";
1242
+ const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : back;
1243
+ return refuse(response, who, result.status, result.message, retryBack);
1323
1244
  }
1324
- const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
1325
- if (!filed.ok)
1326
- return refuse(response, who, 409, filed.message, back);
1327
- return redirect(response, taskHref(filed.id));
1245
+ return redirect(response, revisionDestination(result.id, returnTo));
1328
1246
  }
1329
- const draftRepair = /^\/r\/([0-9]{1,15})\/draft-repair$/.exec(url.pathname);
1330
- if (draftRepair !== null) {
1331
- // CI repair, suggestion-first (M8.18): a red episode never spawns an
1332
- // agent by itself — it EARNS a button, and the button creates one
1333
- // unapproved task through the same revision machinery as review
1334
- // comments. Deterministic id = one draft per task/PR, ever.
1335
- const id = Number(draftRepair[1]);
1336
- const found = store.getRun(id);
1337
- if (found === null || !visible(taskRepoOf(found.taskRef))) {
1338
- return refuse(response, who, 404, "no such run");
1339
- }
1340
- const publication = store.publicationForRun(id);
1341
- if (publication === null || publication.prNumber === null) {
1342
- return refuse(response, who, 400, "this run published no pull request", `/r/${id}`);
1343
- }
1344
- if (!store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)) {
1345
- return refuse(response, who, 400, "no failing CI is observed on this PR right now", `/r/${id}`);
1346
- }
1347
- const sourceTaskId = store.externalIdFor(found.taskRef) ?? "?";
1348
- const sourceScope = store.getScope(sourceTaskId);
1349
- // The observed episode, not the click: the brief binds the head the
1350
- // failure was SEEN on and when (audit C-2) — a PR that advanced since
1351
- // is a different failure, and the click time is not an observation.
1352
- const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
1353
- // Suffixes survive truncation (audit C-7): the prefix gives way, the
1354
- // identity-bearing tail never does.
1355
- const suffix = `-ci-${publication.prNumber}`;
1356
- const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
1357
- const brief = {
1358
- schema: 1,
1359
- kind: "ci-repair",
1360
- sourceTask: sourceTaskId,
1361
- sourceRun: id,
1362
- sourceScopeDigest: sourceScope?.digest ?? null,
1363
- head: found.headRevision,
1364
- pr: publication.prNumber,
1365
- prUrl: publication.prUrl,
1366
- publishedHeadSha: publication.headSha,
1367
- observedFailingHead: episode?.headSha ?? null,
1368
- observedAt: episode?.createdAt ?? null,
1369
- };
1370
- const briefBytes = Buffer.from(JSON.stringify(brief, null, 2), "utf8");
1371
- const briefName = `ci-repair-brief-${randomBytes(6).toString("hex")}.json`;
1372
- const key = writeEvidenceFile(evidenceRoot, id, briefName, briefBytes);
1373
- // The SAME revision boundary the annotation road and the criterion
1374
- // repair use (contract handoff task 2): the inherited terms come from
1375
- // the source rows inside the seal; the scope read above only names
1376
- // the digest this draft was composed against.
1377
- const sealed = store.sealRevision({
1378
- source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
1379
- brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
1380
- child: {
1381
- id: draftId,
1382
- title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
1383
- repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
1384
- `Read the failing checks on GitHub before approving; this draft carries no log content.`,
1385
- },
1386
- commentIds: null,
1387
- requestedBy: who.name,
1388
- }, now);
1389
- if (!sealed.ok) {
1390
- return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${draftId}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
1247
+ const terminal = store.artifactsFor(id).find(one => one.kind === "terminal-diff");
1248
+ if (terminal === undefined) {
1249
+ return refuse(response, who, 400, "this run has no terminal diff to comment on", back);
1250
+ }
1251
+ // The bytes must VERIFY before words attach to them (audit IV-10):
1252
+ // "a comment on the exact reviewed bytes" is a lie if the bytes are
1253
+ // gone or no longer hash to their record.
1254
+ const proven = readVerifiedArtifact(evidenceRoot, terminal);
1255
+ if (!proven.ok) {
1256
+ return refuse(response, who, 409, `the terminal diff no longer verifies (${proven.problem}) — nothing to comment on`, back);
1257
+ }
1258
+ const note = validateNote(body.get("note") ?? "");
1259
+ if (!note.ok)
1260
+ return refuse(response, who, 400, note.problem, back);
1261
+ const rawPath = (body.get("path") ?? "").trim();
1262
+ if (rawPath.length > 300 || hasForbiddenControls(rawPath)) {
1263
+ return refuse(response, who, 400, "Enter a file path of up to 300 characters, without control characters.", back);
1264
+ }
1265
+ const rawLine = (body.get("line") ?? "").trim();
1266
+ const line = rawLine === "" ? null : Number(rawLine);
1267
+ if (line !== null && (!Number.isInteger(line) || line < 1 || line > 1_000_000)) {
1268
+ return refuse(response, who, 400, "Enter a whole line number from 1 to 1,000,000, or leave it blank.", back);
1269
+ }
1270
+ // The form's own request token (package 3) is the dedupe key: a
1271
+ // replayed or double submission finds its note already recorded and
1272
+ // lands on the same receipt instead of a second note. The token is
1273
+ // bound to the account, so nobody else's replay can consume it.
1274
+ const request = body.get("request");
1275
+ const sourceKey = commentSourceKey(who.name, request);
1276
+ const path = rawPath === "" ? null : rawPath;
1277
+ const inserted = store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, ...(sourceKey === undefined ? {} : { sourceKey }) }, now);
1278
+ if (inserted === null && sourceKey !== undefined) {
1279
+ // The token was used before (repair 2026-09-14, finding 1). A
1280
+ // request identity is IMMUTABLE: it names exactly one note on
1281
+ // exactly one result. The unchanged retry — same run, same file,
1282
+ // line and words — lands on its original receipt; any other reuse
1283
+ // is refused, and the refusal's way back carries `conflict=<token>`
1284
+ // so the browser mints a fresh identity for the words it still
1285
+ // holds. Nothing typed is lost either way.
1286
+ const earlier = store.diffCommentBySourceKey(sourceKey);
1287
+ const conflictBack = `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes`;
1288
+ if (earlier === null || earlier.run !== id) {
1289
+ return refuse(response, who, 409, `this note's request identity was already used on another result (build #${earlier === null ? "?" : earlier.run}) — go back; your words are kept, and the form will carry a new identity`, conflictBack);
1290
+ }
1291
+ if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note) {
1292
+ return refuse(response, who, 409, `this request identity already recorded a different note on this result (${earlier.path === null ? "no file" : `${earlier.path}${earlier.line === null ? "" : `:${earlier.line}`}`}: "${oneLineOf(earlier.note, 80)}") — go back; your edited words are kept as a new note, and the form will carry a new identity`, conflictBack);
1391
1293
  }
1392
- // The merge blocker rides the SAME breath as the draft (merge grant,
1393
- // findings 3/12/13): while this repair exists, the source PR merges
1394
- // NOTHING — sticky until the operator's unblock act or the PR closes.
1395
- store.createMergeBlocker(publication.id, sealed.id, now);
1396
- return redirect(response, taskHref(sealed.id));
1397
1294
  }
1295
+ // Land back AT the request-changes form with the note field ready —
1296
+ // writing five notes in a row must cost five keystrokes of
1297
+ // navigation, not five scrolls (arc 6, finding 5/6). The receipt
1298
+ // names the request token, so the browser clears exactly the draft
1299
+ // that landed and no other.
1300
+ const receipt = sourceKey === undefined ? "1" : request;
1301
+ return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
1302
+ }
1303
+ async function sessionEditorLinks(ctx) {
1304
+ const { who, response, posted } = ctx;
1305
+ const body = readForm(posted, CONSOLE_FORMS.editorLinks);
1306
+ // The session half of the editor-link activation (arc 6, finding 1):
1307
+ // only the person at the browser can say "this device holds the
1308
+ // worktrees". Per-session, dies with the session, grants nothing —
1309
+ // it only lets already-authorized pages RENDER vscode links.
1310
+ if (who.via !== "cookie")
1311
+ return refuse(response, who, 403, "editor links are a browser session's choice");
1312
+ if (options.editorLinks === undefined)
1313
+ return refuse(response, who, 404, "editor links are not enabled on this server");
1314
+ who.session.editorLinks = body.get("on") === "1";
1315
+ const back = body.get("return") ?? "/";
1316
+ return redirect(response, /^\/[a-z0-9/_-]*$/i.test(back) ? back : "/");
1317
+ }
1318
+ async function runRevise(ctx) {
1319
+ const { url, who, response, now, posted } = ctx;
1320
+ const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
1321
+ if (revise === null)
1322
+ return noPage(who, response);
1323
+ const body = readForm(posted, CONSOLE_FORMS.revise);
1324
+ // Seal the live comment batch into ONE unapproved revision task with
1325
+ // an immutable brief (M6.8). Deterministic — no model reads anything
1326
+ // here — and every revision takes its own approval: comments can
1327
+ // semantically widen work, and no path check can prove they did not.
1328
+ const id = Number(revise[1]);
1329
+ const found = store.getRun(id);
1330
+ if (found === null || !visible(taskRepoOf(found.taskRef))) {
1331
+ return refuse(response, who, 404, "no such run");
1332
+ }
1333
+ const back = resultReturnTarget(body.get("return"), id);
1334
+ const result = createResultRevision(store, evidenceRoot, {
1335
+ run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1336
+ repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
1337
+ }, now);
1338
+ if (!result.ok)
1339
+ return refuse(response, who, result.status, result.message, back);
1340
+ return redirect(response, revisionDestination(result.id, back));
1341
+ }
1342
+ // Follow-ups on a result: Run checks on its exact commit (a worker runs the
1343
+ // approved command once and seals the log), or file a task to add tests.
1344
+ const runFollowUp = (op) => async (ctx) => {
1345
+ const { url, who, response, now, posted } = ctx;
1346
+ const followUp = /^\/r\/([0-9]{1,15})\//.exec(url.pathname);
1347
+ if (followUp === null)
1348
+ return noPage(who, response);
1349
+ const body = readForm(posted, CONSOLE_FORMS.followUp);
1350
+ const id = Number(followUp[1]);
1351
+ const found = store.getRun(id);
1352
+ if (found === null || !visible(taskRepoOf(found.taskRef)))
1353
+ return refuse(response, who, 404, "no such run");
1354
+ // The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
1355
+ const runTask = store.externalIdFor(found.taskRef);
1356
+ const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
1357
+ if (who.via !== "cookie")
1358
+ return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
1359
+ if (op === "checks") {
1360
+ if (who.role !== "approver")
1361
+ return refuse(response, who, 403, "An approver runs checks.", back);
1362
+ if (store.isDemo())
1363
+ return refuse(response, who, 403, "The demo runs no checks.", back);
1364
+ const level = body.get("level") === "full" ? "full" : "quick";
1365
+ const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
1366
+ if (!asked.ok)
1367
+ return refuse(response, who, 409, asked.message, back);
1368
+ // Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
1369
+ return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
1370
+ }
1371
+ const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
1372
+ if (!filed.ok)
1373
+ return refuse(response, who, 409, filed.message, back);
1374
+ return redirect(response, taskHref(filed.id));
1375
+ };
1376
+ async function runDraftRepair(ctx) {
1377
+ const { url, who, response, now } = ctx;
1378
+ const draftRepair = /^\/r\/([0-9]{1,15})\/draft-repair$/.exec(url.pathname);
1379
+ if (draftRepair === null)
1380
+ return noPage(who, response);
1381
+ // CI repair, suggestion-first (M8.18): a red episode never spawns an
1382
+ // agent by itself — it EARNS a button, and the button creates one
1383
+ // unapproved task through the same revision machinery as review
1384
+ // comments. Deterministic id = one draft per task/PR, ever.
1385
+ const id = Number(draftRepair[1]);
1386
+ const found = store.getRun(id);
1387
+ if (found === null || !visible(taskRepoOf(found.taskRef))) {
1388
+ return refuse(response, who, 404, "no such run");
1389
+ }
1390
+ const publication = store.publicationForRun(id);
1391
+ if (publication === null || publication.prNumber === null) {
1392
+ return refuse(response, who, 400, "this run published no pull request", `/r/${id}`);
1393
+ }
1394
+ if (!store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)) {
1395
+ return refuse(response, who, 400, "no failing CI is observed on this PR right now", `/r/${id}`);
1396
+ }
1397
+ const sourceTaskId = store.externalIdFor(found.taskRef) ?? "?";
1398
+ const sourceScope = store.getScope(sourceTaskId);
1399
+ // The observed episode, not the click: the brief binds the head the
1400
+ // failure was SEEN on and when (audit C-2) — a PR that advanced since
1401
+ // is a different failure, and the click time is not an observation.
1402
+ const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
1403
+ // Suffixes survive truncation (audit C-7): the prefix gives way, the
1404
+ // identity-bearing tail never does.
1405
+ const suffix = `-ci-${publication.prNumber}`;
1406
+ const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
1407
+ const brief = {
1408
+ schema: 1,
1409
+ kind: "ci-repair",
1410
+ sourceTask: sourceTaskId,
1411
+ sourceRun: id,
1412
+ sourceScopeDigest: sourceScope?.digest ?? null,
1413
+ head: found.headRevision,
1414
+ pr: publication.prNumber,
1415
+ prUrl: publication.prUrl,
1416
+ publishedHeadSha: publication.headSha,
1417
+ observedFailingHead: episode?.headSha ?? null,
1418
+ observedAt: episode?.createdAt ?? null,
1419
+ };
1420
+ const briefBytes = Buffer.from(JSON.stringify(brief, null, 2), "utf8");
1421
+ const briefName = `ci-repair-brief-${randomBytes(6).toString("hex")}.json`;
1422
+ const key = writeEvidenceFile(evidenceRoot, id, briefName, briefBytes);
1423
+ // The SAME revision boundary the annotation road and the criterion
1424
+ // repair use (contract handoff task 2): the inherited terms come from
1425
+ // the source rows inside the seal; the scope read above only names
1426
+ // the digest this draft was composed against.
1427
+ const sealed = store.sealRevision({
1428
+ source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
1429
+ brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
1430
+ child: {
1431
+ id: draftId,
1432
+ title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
1433
+ repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
1434
+ `Read the failing checks on GitHub before approving; this draft carries no log content.`,
1435
+ },
1436
+ commentIds: null,
1437
+ requestedBy: who.name,
1438
+ }, now);
1439
+ if (!sealed.ok) {
1440
+ return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${draftId}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
1441
+ }
1442
+ // The merge blocker rides the SAME breath as the draft (merge grant,
1443
+ // findings 3/12/13): while this repair exists, the source PR merges
1444
+ // NOTHING — sticky until the operator's unblock act or the PR closes.
1445
+ store.createMergeBlocker(publication.id, sealed.id, now);
1446
+ return redirect(response, taskHref(sealed.id));
1447
+ }
1448
+ async function resolveIncident(ctx) {
1449
+ const { url, who, response, now, posted } = ctx;
1398
1450
  const resolve = /^\/i\/([0-9]{1,15})\/resolve$/.exec(url.pathname);
1399
- if (resolve !== null) {
1400
- const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
1401
- const id = Number(resolve[1]);
1402
- // The ceiling applies to incident mutation exactly as to every other
1403
- // resource (v3 review, finding 3): resolve incident → run → task, and
1404
- // an incident outside this server's scope does not exist here. The
1405
- // task id is captured BEFORE resolving — afterwards the incident is
1406
- // no longer open and could not be found again.
1407
- const openRow = store.openIncidents().find(one => one.id === id);
1408
- const incidentRun = openRow === undefined ? null : store.getRun(openRow.run);
1409
- if (openRow !== undefined && incidentRun !== null && !visible(taskRepoOf(incidentRun.taskRef))) {
1410
- return refuse(response, who, 404, "no such incident");
1411
- }
1412
- const resolved = store.resolveIncident(id, who.name, now);
1413
- if (!resolved)
1414
- return refuse(response, who, 409, "already resolved, or never open");
1415
- const back = body.get("return") === "inbox" ? "/inbox" : openRow === undefined ? "/" : taskHref(openRow.taskId);
1416
- return redirect(response, back);
1451
+ if (resolve === null)
1452
+ return noPage(who, response);
1453
+ const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
1454
+ const id = Number(resolve[1]);
1455
+ // The ceiling applies to incident mutation exactly as to every other
1456
+ // resource (v3 review, finding 3): resolve incident → run → task, and
1457
+ // an incident outside this server's scope does not exist here. The
1458
+ // task id is captured BEFORE resolving — afterwards the incident is
1459
+ // no longer open and could not be found again.
1460
+ const openRow = store.openIncidents().find(one => one.id === id);
1461
+ const incidentRun = openRow === undefined ? null : store.getRun(openRow.run);
1462
+ if (openRow !== undefined && incidentRun !== null && !visible(taskRepoOf(incidentRun.taskRef))) {
1463
+ return refuse(response, who, 404, "no such incident");
1417
1464
  }
1418
- return refuse(response, who, 404, "There's no page at this address.", "/chat");
1465
+ const resolved = store.resolveIncident(id, who.name, now);
1466
+ if (!resolved)
1467
+ return refuse(response, who, 409, "already resolved, or never open");
1468
+ const back = body.get("return") === "inbox" ? "/inbox" : openRow === undefined ? "/" : taskHref(openRow.taskId);
1469
+ return redirect(response, back);
1419
1470
  }
1420
1471
  /** The compact recent-runs list for the master pane. */
1421
1472
  function runListPane(project, currentId) {
1422
1473
  const rows = store.listRunsBefore(null, 50, project);
1423
1474
  const live = liveRunIds(rows);
1424
- const items = rows
1425
- .map(run => `<a class="item${run.id === currentId ? " current" : ""}" href="/r/${run.id}">` +
1426
- `<span class="t">#${run.id} \u00b7 ${escape(run.taskId)}</span>` +
1427
- `<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
1428
- `<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
1429
- .join("\n");
1430
- return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
1475
+ const items = joinHtml(rows
1476
+ .map(run => html `<a class="item${run.id === currentId ? " current" : ""}" href="/r/${run.id}"><span class="t">#${run.id} \u00b7 ${run.taskId}</span><span class="m">${runOutcomeBadge(run, live.has(run.id))}<span class="mono">${whenTime(run.startedAt)}</span></span></a>`), "\n");
1477
+ return html `<h2>Builds</h2>\n${rows.length === 0 ? html `<p class="meta">None yet</p>` : items}`;
1431
1478
  }
1432
1479
  async function peekFragment(runId, sessionKey, editorMode = false) {
1480
+ // The cache and the in-flight coalescer hold the fragment's bytes (runtime's types); they are markup this made.
1481
+ const said = (message, final) => peekSay(message, final);
1433
1482
  const guarded = peekGuards(runId);
1434
1483
  if (!guarded.ok)
1435
- return { status: 200, body: peekSay(guarded.message, guarded.final === true) };
1484
+ return { status: 200, body: said(guarded.message, guarded.final === true) };
1436
1485
  const { run, worktree, epoch, entries } = guarded.admit;
1437
1486
  // The cache and the in-flight coalescer both vary by LINK MODE (arc 6,
1438
1487
  // finding 3): a linked fragment rendered for one session must never be
@@ -1444,17 +1493,17 @@ export function createTasksHandlers(runtime) {
1444
1493
  }
1445
1494
  if (cached !== undefined) {
1446
1495
  peekCache.delete(key);
1447
- runtime.peekCacheBytes -= Buffer.byteLength(cached.fragment);
1496
+ runtime.peekCacheBytes -= cached.bytes;
1448
1497
  }
1449
1498
  // Coalesce per run; bound per session and globally (finding 10).
1450
1499
  const flightKey = `${runId}:${editorMode ? "links" : "plain"}`;
1451
1500
  const inFlight = peekInFlight.get(flightKey);
1452
1501
  if (inFlight !== undefined)
1453
- return { status: 200, body: await inFlight };
1502
+ return { status: 200, body: (await inFlight) };
1454
1503
  if (peekInFlight.size >= PEEK_GLOBAL_INFLIGHT)
1455
- return { status: 429, body: peekSay("the live view is busy — it retries by itself"), retryAfter: 10 };
1504
+ return { status: 429, body: said("the live view is busy — it retries by itself"), retryAfter: 10 };
1456
1505
  if ((peekBySession.get(sessionKey) ?? 0) >= PEEK_SESSION_INFLIGHT) {
1457
- return { status: 429, body: peekSay("too many live views from this session"), retryAfter: 10 };
1506
+ return { status: 429, body: said("too many live views from this session"), retryAfter: 10 };
1458
1507
  }
1459
1508
  peekBySession.set(sessionKey, (peekBySession.get(sessionKey) ?? 0) + 1);
1460
1509
  const work = (async () => {
@@ -1470,64 +1519,64 @@ export function createTasksHandlers(runtime) {
1470
1519
  return peekSay(seen.reason);
1471
1520
  const stamp = clock().toISOString().slice(11, 19);
1472
1521
  const parts = [
1473
- `<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
1522
+ html `<p class="meta">Best-effort look at ${stamp} UTC — files can change mid-read</p>`,
1474
1523
  ];
1475
1524
  const changed = seen.rows.filter(one => one.kind === "changed");
1476
1525
  const deleted = seen.rows.filter(one => one.kind === "deleted");
1477
1526
  const unchecked = seen.rows.filter(one => one.kind === "unchecked");
1478
1527
  const fresh = aggregateNewNames(seen.newPaths);
1479
1528
  if (changed.length === 0 && deleted.length === 0 && fresh.total === 0) {
1480
- parts.push(`<p class="row">nothing has changed against the starting point yet</p>`);
1529
+ parts.push(html `<p class="row">nothing has changed against the starting point yet</p>`);
1481
1530
  }
1482
1531
  // A name is linked ONLY when sanitize provably changed nothing (arc 6,
1483
1532
  // finding 3): a masked or normalized label must never carry an href
1484
1533
  // that discloses what the mask hid. Collapsed labels never link.
1534
+ const shownName = (path) => peekName(path);
1485
1535
  const linkedName = (path) => {
1486
- const shown = peekName(path);
1487
- if (!editorMode || shown !== escape(path))
1536
+ const shown = shownName(path);
1537
+ if (!editorMode || htmlString(shown) !== htmlString(textHtml(path)))
1488
1538
  return shown;
1489
1539
  const href = editorFileHref(worktree, path);
1490
- return href === null ? shown : `<a href="${escape(href)}">${shown}</a>`;
1540
+ return href === null ? shown : html `<a href="${href}">${shown}</a>`;
1491
1541
  };
1492
- const line = (row, mark) => `<p class="row mono">${mark} ${linkedName(row.path)} <span class="meta">${escape(row.detail)}</span></p>`;
1542
+ const line = (row, mark) => html `<p class="row mono">${mark} ${linkedName(row.path)} <span class="meta">${row.detail}</span></p>`;
1493
1543
  for (const row of changed)
1494
1544
  parts.push(line(row, "~"));
1495
1545
  for (const row of deleted)
1496
1546
  parts.push(line(row, "−"));
1497
1547
  if (fresh.total > 0) {
1498
- parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
1548
+ parts.push(html `<p class="meta">New files · ${fresh.total}</p>`);
1499
1549
  for (const row of fresh.rows) {
1500
1550
  parts.push(row.collapsed
1501
- ? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
1502
- : `<p class="row mono">+ ${peekName(row.label)}</p>`);
1551
+ ? html `<p class="row mono">+ ${shownName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
1552
+ : html `<p class="row mono">+ ${shownName(row.label)}</p>`);
1503
1553
  }
1504
1554
  if (fresh.renderedFiles < fresh.total) {
1505
- parts.push(`<p class="meta">…and ${fresh.total - fresh.renderedFiles} more (${fresh.total} new files total)</p>`);
1555
+ parts.push(html `<p class="meta">…and ${fresh.total - fresh.renderedFiles} more (${fresh.total} new files total)</p>`);
1506
1556
  }
1507
1557
  }
1508
1558
  if (unchecked.length > 0) {
1509
- parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
1559
+ parts.push(html `<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
1510
1560
  for (const row of unchecked)
1511
1561
  parts.push(line(row, "?"));
1512
1562
  }
1513
1563
  if (seen.partial !== null)
1514
- parts.push(`<p class="meta">${escape(seen.partial)}</p>`);
1515
- let fragment = parts.join("\n");
1516
- if (Buffer.byteLength(fragment) > PEEK_FRAGMENT_BYTES) {
1564
+ parts.push(html `<p class="meta">${seen.partial}</p>`);
1565
+ let fragment = joinHtml(parts, "\n");
1566
+ if (Buffer.byteLength(htmlString(fragment)) > PEEK_FRAGMENT_BYTES) {
1517
1567
  // The byte cap is enforced AFTER escaping (finding 32): an oversize
1518
1568
  // rendering is replaced whole by its exact counts.
1519
- fragment =
1520
- `<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
1521
- `<p class="row">${changed.length} changed · ${deleted.length} deleted · ${fresh.total} new · ${unchecked.length} unverified — too much to render live; the final diff will hold the detail</p>`;
1569
+ fragment = html `<p class="meta">Best-effort look at ${stamp} UTC</p><p class="row">${changed.length} changed · ${deleted.length} deleted · ${fresh.total} new · ${unchecked.length} unverified — too much to render live; the final diff will hold the detail</p>`;
1522
1570
  }
1523
- peekCache.set(key, { fragment, at: Date.now() });
1524
- runtime.peekCacheBytes += Buffer.byteLength(fragment);
1571
+ const bytes = Buffer.byteLength(htmlString(fragment));
1572
+ peekCache.set(key, { fragment, bytes, at: Date.now() });
1573
+ runtime.peekCacheBytes += bytes;
1525
1574
  peekEvict();
1526
1575
  return fragment;
1527
1576
  })();
1528
1577
  peekInFlight.set(flightKey, work);
1529
1578
  try {
1530
- return { status: 200, body: await work };
1579
+ return { status: 200, body: (await work) };
1531
1580
  }
1532
1581
  finally {
1533
1582
  peekInFlight.delete(flightKey);
@@ -2177,9 +2226,8 @@ export function createTasksHandlers(runtime) {
2177
2226
  }
2178
2227
  case "complete": {
2179
2228
  const body = readForm(posted, CONSOLE_FORMS.taskComplete);
2180
- if (who.via !== "cookie" || who.role !== "approver")
2181
- return refuse(response, who, 403, "Only an approver can mark a result complete.");
2182
- const principal = matePrincipal(who);
2229
+ // The row admits only an approver's browser session; the principal re-proves that session's standing.
2230
+ const principal = who.via === "cookie" ? matePrincipal(who) : null;
2183
2231
  if (principal === null)
2184
2232
  return refuse(response, who, 403, "Your access changed. Sign in again.");
2185
2233
  const digest = body.get("receipt") ?? "";
@@ -2241,11 +2289,11 @@ export function createTasksHandlers(runtime) {
2241
2289
  const asked = requestTaskStop(store, { taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web" }, now);
2242
2290
  if (!asked.ok) {
2243
2291
  const words = {
2244
- "no-run": `there is no run #${escape(named)}`,
2245
- "wrong-task": `run #${escape(named)} is not one of this task's attempts`,
2246
- finished: `run #${escape(named)} already ended before the stop — nothing rewrites a finished attempt; reload to see the current state`,
2247
- "not-live": `run #${escape(named)} is not the attempt holding this task now — reload and decide against the current attempt`,
2248
- publication: `run #${escape(named)} already admitted its publication — an external request in flight is not recalled by a stop`,
2292
+ "no-run": `there is no run #${named}`,
2293
+ "wrong-task": `run #${named} is not one of this task's attempts`,
2294
+ finished: `run #${named} already ended before the stop — nothing rewrites a finished attempt; reload to see the current state`,
2295
+ "not-live": `run #${named} is not the attempt holding this task now — reload and decide against the current attempt`,
2296
+ publication: `run #${named} already admitted its publication — an external request in flight is not recalled by a stop`,
2249
2297
  };
2250
2298
  return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, 409);
2251
2299
  }
@@ -2334,7 +2382,7 @@ export function createTasksHandlers(runtime) {
2334
2382
  capacity: one.capacity,
2335
2383
  building: building.get(one.name) ?? 0,
2336
2384
  }));
2337
- return queueBody(tasks, workers, csrf, revision, store.queueRevision());
2385
+ return queueBody(tasks, workers, revision, store.queueRevision());
2338
2386
  }
2339
2387
  /** An OBSERVED CI failure on a completed row's PR — the open episode the
2340
2388
  * watcher recorded for exactly that repository and number; never a guess
@@ -2612,58 +2660,78 @@ export function createTasksHandlers(runtime) {
2612
2660
  live.add(run.id);
2613
2661
  return live;
2614
2662
  }
2615
- const registrations = [
2616
- { id: "code.page", domain: "tasks", stage: "console", method: "GET", handle: get },
2617
- { id: "inbox", domain: "tasks", stage: "console", method: "GET", handle: get },
2618
- { id: "work", domain: "tasks", stage: "console", method: "GET", handle: get },
2619
- { id: "next", domain: "tasks", stage: "console", method: "GET", handle: get },
2620
- { id: "board", domain: "tasks", stage: "console", method: "GET", handle: get },
2621
- { id: "review", domain: "tasks", stage: "console", method: "GET", handle: get },
2622
- { id: "done", domain: "tasks", stage: "console", method: "GET", handle: get },
2623
- { id: "tasks", domain: "tasks", stage: "console", method: "GET", handle: get },
2624
- { id: "queue", domain: "tasks", stage: "console", method: "GET", handle: get },
2625
- { id: "tasks.new", domain: "tasks", stage: "console", method: "GET", handle: get },
2626
- { id: "task.live", domain: "tasks", stage: "console", method: "GET", handle: get },
2627
- { id: "task.page", domain: "tasks", stage: "console", method: "GET", handle: get },
2628
- { id: "task.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
2629
- { id: "runs", domain: "tasks", stage: "console", method: "GET", handle: get },
2630
- { id: "run.page", domain: "tasks", stage: "console", method: "GET", handle: get },
2631
- { id: "run.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
2632
- { id: "routines", domain: "tasks", stage: "console", method: "GET", handle: get },
2633
- { id: "routine.page", domain: "tasks", stage: "console", method: "GET", handle: get },
2634
- { id: "decision.page", domain: "tasks", stage: "console", method: "GET", handle: get },
2635
- { id: "decision.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
2636
- { id: "code.act", domain: "tasks", stage: "console", method: "POST", handle: post },
2637
- { id: "tasks.add", domain: "tasks", stage: "console", method: "POST", handle: post },
2638
- { id: "queue.move", domain: "tasks", stage: "console", method: "POST", handle: post },
2639
- { id: "queue.note", domain: "tasks", stage: "console", method: "POST", handle: post },
2640
- { id: "decision.answer", domain: "tasks", stage: "console", method: "POST", handle: post },
2641
- { id: "task.act.hold", domain: "tasks", stage: "console", method: "POST", handle: post },
2642
- { id: "task.act.unhold", domain: "tasks", stage: "console", method: "POST", handle: post },
2643
- { id: "task.act.requeue", domain: "tasks", stage: "console", method: "POST", handle: post },
2644
- { id: "task.act.cancel", domain: "tasks", stage: "console", method: "POST", handle: post },
2645
- { id: "task.act.scope", domain: "tasks", stage: "console", method: "POST", handle: post },
2646
- { id: "task.act.approve", domain: "tasks", stage: "console", method: "POST", handle: post },
2647
- { id: "task.act.plan", domain: "tasks", stage: "console", method: "POST", handle: post },
2648
- { id: "task.act.plan-edit", domain: "tasks", stage: "console", method: "POST", handle: post },
2649
- { id: "task.act.next", domain: "tasks", stage: "console", method: "POST", handle: post },
2650
- { id: "task.act.reopen", domain: "tasks", stage: "console", method: "POST", handle: post },
2651
- { id: "task.act.steer", domain: "tasks", stage: "console", method: "POST", handle: post },
2652
- { id: "task.act.accept-proof", domain: "tasks", stage: "console", method: "POST", handle: post },
2653
- { id: "task.act.accept-revision", domain: "tasks", stage: "console", method: "POST", handle: post },
2654
- { id: "task.act.reject-revision", domain: "tasks", stage: "console", method: "POST", handle: post },
2655
- { id: "task.act.route", domain: "tasks", stage: "console", method: "POST", handle: post },
2656
- { id: "task.act.retry-review", domain: "tasks", stage: "console", method: "POST", handle: post },
2657
- { id: "task.act.complete", domain: "tasks", stage: "console", method: "POST", handle: post },
2658
- { id: "task.act.merge", domain: "tasks", stage: "console", method: "POST", handle: post },
2659
- { id: "task.act.confirm-stopped", domain: "tasks", stage: "console", method: "POST", handle: post },
2660
- { id: "task.act.stop", domain: "tasks", stage: "console", method: "POST", handle: post },
2661
- { id: "task.act.resume-arm", domain: "tasks", stage: "console", method: "POST", handle: post },
2662
- { id: "task.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
2663
- { id: "task.instance-act", domain: "tasks", stage: "console", method: "POST", handle: post },
2664
- { id: "run.act", domain: "tasks", stage: "console", method: "POST", handle: post },
2665
- { id: "session.editor-links", domain: "tasks", stage: "console", method: "POST", handle: post },
2666
- { id: "incident.resolve", domain: "tasks", stage: "console", method: "POST", handle: post },
2667
- ];
2668
- return { registrations, get, post, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
2663
+ const registrations = handlersOf("tasks", {
2664
+ "code.page": showCode("workspace"),
2665
+ "code.session": showCode("session"),
2666
+ "code.state": showCode("state"),
2667
+ "code.changes": showCode("changes"),
2668
+ "code.ship": showCode("ship"),
2669
+ "code.other": showCode("unknown"),
2670
+ "inbox": showInbox,
2671
+ "work": showWork,
2672
+ "next": showNext,
2673
+ "board": showBoard,
2674
+ "review": showReview,
2675
+ "done": showDone,
2676
+ "tasks": showTasks,
2677
+ "queue": showQueue,
2678
+ "tasks.new": showNewTask,
2679
+ "task.page": showTask,
2680
+ "task.evidence": showTaskEvidence,
2681
+ "runs": showRuns,
2682
+ "run.page": showRun,
2683
+ "run.evidence": showRunEvidence,
2684
+ "routines": showRoutines,
2685
+ "routine.page": showRoutine,
2686
+ "decision.page": showDecision,
2687
+ "decision.evidence": showDecisionEvidence,
2688
+ "code.start": codeAct("start"),
2689
+ "code.send-send": codeAct("send"),
2690
+ "code.stop-send": codeAct("stop"),
2691
+ "code.resume-send": codeAct("resume"),
2692
+ "code.recover-send": codeAct("recover"),
2693
+ "code.continue-send": codeAct("continue"),
2694
+ "code.ship-send": codeAct("ship"),
2695
+ "code.answer-send": codeAct("answer"),
2696
+ "code.act-other": codeAct("unknown"),
2697
+ "tasks.add": addTask,
2698
+ "queue.move": queueMove,
2699
+ "queue.note": queueNote,
2700
+ "decision.answer": answerDecision,
2701
+ "task.act.hold": taskAct("hold"),
2702
+ "task.act.unhold": taskAct("unhold"),
2703
+ "task.act.requeue": taskAct("requeue"),
2704
+ "task.act.cancel": taskAct("cancel"),
2705
+ "task.act.scope": taskAct("scope"),
2706
+ "task.act.approve": taskAct("approve"),
2707
+ "task.act.plan": taskAct("plan"),
2708
+ "task.act.plan-edit": taskAct("plan-edit"),
2709
+ "task.act.next": taskAct("next"),
2710
+ "task.act.reopen": taskAct("reopen"),
2711
+ "task.act.steer": taskAct("steer"),
2712
+ "task.act.accept-proof": taskAct("accept-proof"),
2713
+ "task.act.accept-revision": taskAct("accept-revision"),
2714
+ "task.act.reject-revision": taskAct("reject-revision"),
2715
+ "task.act.route": taskAct("route"),
2716
+ "task.act.retry-review": taskAct("retry-review"),
2717
+ "task.act.complete": taskAct("complete"),
2718
+ "task.act.merge": taskMerge,
2719
+ "task.act.confirm-stopped": taskConfirmStopped,
2720
+ "task.act.stop": taskAct("stop"),
2721
+ "task.act.resume-arm": taskAct("resume-arm"),
2722
+ "task.act.resume": taskAct("resume"),
2723
+ "task.act.block": taskAct("block"),
2724
+ "task.act.unblock": taskAct("unblock"),
2725
+ "task.act.repair-dependency": taskAct("repair-dependency"),
2726
+ "task.act.follow-up": taskAct("follow-up"),
2727
+ "run.act.note": runNote,
2728
+ "run.act.comment": runComment,
2729
+ "run.act.revise": runRevise,
2730
+ "run.act.draft-repair": runDraftRepair,
2731
+ "run.act.checks": runFollowUp("checks"),
2732
+ "run.act.add-tests": runFollowUp("add-tests"),
2733
+ "session.editor-links": sessionEditorLinks,
2734
+ "incident.resolve": resolveIncident,
2735
+ });
2736
+ return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
2669
2737
  }