toolroll 0.9.93 → 0.9.94

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 (328) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +4 -0
  5. package/dist/agent-fence.js +4 -1
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-ui.js +1 -1
  11. package/dist/assignment.js +5 -0
  12. package/dist/boot-identity.d.ts +2 -1
  13. package/dist/boot-identity.js +6 -4
  14. package/dist/bounded-spawn.d.ts +57 -0
  15. package/dist/bounded-spawn.js +220 -0
  16. package/dist/browser/terminal.js +42 -42
  17. package/dist/browser/workspace.css +1 -1
  18. package/dist/browser/workspace.js +32 -32
  19. package/dist/browser-shell.d.ts +1 -1
  20. package/dist/browser-shell.js +2 -2
  21. package/dist/browser-workspace.d.ts +45 -2
  22. package/dist/browser-workspace.js +3 -3
  23. package/dist/builder-restart.d.ts +21 -0
  24. package/dist/builder-restart.js +50 -0
  25. package/dist/builder.d.ts +4 -0
  26. package/dist/builder.js +53 -24
  27. package/dist/chat-acceptance.js +1 -1
  28. package/dist/chat-actions.d.ts +12 -2
  29. package/dist/chat-actions.js +25 -2
  30. package/dist/chat-channel.js +11 -0
  31. package/dist/chat-decide.d.ts +7 -2
  32. package/dist/chat-decide.js +37 -9
  33. package/dist/chat-quiet.js +3 -3
  34. package/dist/chat-voice.d.ts +1 -1
  35. package/dist/chat-voice.js +2 -2
  36. package/dist/check-facts.d.ts +3 -1
  37. package/dist/check-facts.js +57 -11
  38. package/dist/check-failure.d.ts +22 -3
  39. package/dist/check-failure.js +57 -13
  40. package/dist/check-levels-ui.d.ts +4 -0
  41. package/dist/check-levels-ui.js +10 -1
  42. package/dist/check-load.d.ts +85 -0
  43. package/dist/check-load.js +147 -0
  44. package/dist/check-only-standing.d.ts +36 -0
  45. package/dist/check-only-standing.js +39 -0
  46. package/dist/check-only.d.ts +15 -0
  47. package/dist/check-only.js +99 -0
  48. package/dist/check-report.d.ts +2 -0
  49. package/dist/check-report.js +1 -1
  50. package/dist/check-stamp.d.ts +46 -0
  51. package/dist/check-stamp.js +93 -0
  52. package/dist/checkout-cleanup.d.ts +36 -5
  53. package/dist/checkout-cleanup.js +166 -14
  54. package/dist/cli-help.d.ts +4 -1
  55. package/dist/cli-help.js +47 -30
  56. package/dist/cli.js +11 -1
  57. package/dist/code-extract.d.ts +44 -0
  58. package/dist/code-extract.js +88 -0
  59. package/dist/code-map-acts.d.ts +24 -0
  60. package/dist/code-map-acts.js +53 -0
  61. package/dist/code-map-admission.d.ts +52 -0
  62. package/dist/code-map-admission.js +295 -0
  63. package/dist/code-map-build.d.ts +63 -0
  64. package/dist/code-map-build.js +415 -0
  65. package/dist/code-map-cli.d.ts +153 -0
  66. package/dist/code-map-cli.js +103 -0
  67. package/dist/code-map-main.d.ts +1 -0
  68. package/dist/code-map-main.js +48 -0
  69. package/dist/code-map-read.d.ts +106 -0
  70. package/dist/code-map-read.js +171 -0
  71. package/dist/code-map-report.d.ts +37 -0
  72. package/dist/code-map-report.js +95 -0
  73. package/dist/code-map-shape.d.ts +172 -0
  74. package/dist/code-map-shape.js +327 -0
  75. package/dist/code-map-store.d.ts +136 -0
  76. package/dist/code-map-store.js +233 -0
  77. package/dist/coding-provider.d.ts +2 -0
  78. package/dist/coding-provider.js +3 -2
  79. package/dist/coding-update.js +3 -2
  80. package/dist/coding-workspace.js +4 -4
  81. package/dist/console-chrome.js +2 -2
  82. package/dist/containment.d.ts +5 -0
  83. package/dist/containment.js +14 -1
  84. package/dist/contracts/chat-actions.d.ts +9 -0
  85. package/dist/contracts/chat-actions.js +2 -0
  86. package/dist/contracts/cli.d.ts +52 -0
  87. package/dist/contracts/cli.js +4 -0
  88. package/dist/contracts/console-api.d.ts +10 -3
  89. package/dist/contracts/console-api.js +10 -3
  90. package/dist/contracts/gateway-tools.d.ts +17 -0
  91. package/dist/contracts/gateway-tools.js +3 -1
  92. package/dist/contracts/lead-tools.d.ts +61 -0
  93. package/dist/contracts/lead-tools.js +10 -1
  94. package/dist/contracts/project-knowledge.d.ts +38 -5
  95. package/dist/contracts/project-knowledge.js +11 -5
  96. package/dist/contracts/project-memory.d.ts +3 -1
  97. package/dist/contracts/project-memory.js +3 -2
  98. package/dist/contracts/storage.d.ts +14 -0
  99. package/dist/contracts/storage.js +3 -1
  100. package/dist/contracts/store-json.d.ts +2 -0
  101. package/dist/contracts/youiyoux-request.d.ts +1 -1
  102. package/dist/contracts/youiyoux-request.js +1 -1
  103. package/dist/desktop-access.js +6 -2
  104. package/dist/dispatch.js +18 -15
  105. package/dist/dispose.js +8 -2
  106. package/dist/easy-start.js +2 -3
  107. package/dist/evals.js +2 -1
  108. package/dist/fast-start.d.ts +136 -0
  109. package/dist/fast-start.js +431 -0
  110. package/dist/fast-terms.d.ts +9 -0
  111. package/dist/fast-terms.js +23 -0
  112. package/dist/fenced-check-coverage.d.ts +62 -0
  113. package/dist/fenced-check-coverage.js +54 -0
  114. package/dist/first-run.js +1 -1
  115. package/dist/flaky-ci.d.ts +31 -0
  116. package/dist/flaky-ci.js +149 -0
  117. package/dist/flaky-cli.d.ts +60 -0
  118. package/dist/flaky-cli.js +60 -0
  119. package/dist/flaky.d.ts +75 -9
  120. package/dist/flaky.js +255 -0
  121. package/dist/flow-card-task.js +3 -0
  122. package/dist/flow-triggers.d.ts +1 -1
  123. package/dist/flow-triggers.js +1 -1
  124. package/dist/github-import.d.ts +62 -0
  125. package/dist/github-import.js +146 -0
  126. package/dist/guides.js +15 -10
  127. package/dist/invoke.d.ts +5 -1
  128. package/dist/invoke.js +49 -14
  129. package/dist/job-object-helper.ps1 +10 -3
  130. package/dist/keymap.d.ts +41 -0
  131. package/dist/keymap.js +51 -0
  132. package/dist/knowledge-cli.js +1 -1
  133. package/dist/knowledge-ui.d.ts +21 -4
  134. package/dist/knowledge-ui.js +75 -7
  135. package/dist/laptop-background.d.ts +16 -0
  136. package/dist/laptop-background.js +108 -2
  137. package/dist/lead-context.js +1 -1
  138. package/dist/lead-contract.d.ts +1 -1
  139. package/dist/lead-contract.js +2 -2
  140. package/dist/lead-progress.js +3 -0
  141. package/dist/lead-status.d.ts +2 -0
  142. package/dist/lead-status.js +6 -2
  143. package/dist/lead-tools.js +51 -2
  144. package/dist/lead.js +1 -1
  145. package/dist/lesson-notes.d.ts +81 -0
  146. package/dist/lesson-notes.js +336 -0
  147. package/dist/limits-ui.js +1 -1
  148. package/dist/local-owner.d.ts +1 -1
  149. package/dist/local-owner.js +11 -0
  150. package/dist/machine-check.d.ts +16 -0
  151. package/dist/machine-check.js +26 -0
  152. package/dist/machine-identity.d.ts +75 -0
  153. package/dist/machine-identity.js +170 -0
  154. package/dist/mcp-core.js +11 -1
  155. package/dist/memory-cli.d.ts +300 -1
  156. package/dist/memory-cli.js +62 -9
  157. package/dist/memory-pass.d.ts +16 -0
  158. package/dist/memory-pass.js +67 -3
  159. package/dist/memory-tidy.d.ts +181 -0
  160. package/dist/memory-tidy.js +523 -0
  161. package/dist/memory-use.d.ts +35 -0
  162. package/dist/memory-use.js +109 -0
  163. package/dist/merge-checklist.d.ts +55 -0
  164. package/dist/merge-checklist.js +182 -0
  165. package/dist/needs-you.d.ts +27 -8
  166. package/dist/needs-you.js +96 -39
  167. package/dist/nightly-github.d.ts +2 -0
  168. package/dist/nightly-github.js +4 -4
  169. package/dist/operate-remote-arguments.js +6 -1
  170. package/dist/operate-remote.js +3 -1
  171. package/dist/operate.d.ts +6 -1
  172. package/dist/operate.js +402 -52
  173. package/dist/phase-routing.js +2 -2
  174. package/dist/plain-schema.d.ts +23 -0
  175. package/dist/plain-schema.js +94 -0
  176. package/dist/plain-words.d.ts +20 -0
  177. package/dist/plain-words.js +66 -0
  178. package/dist/planner.js +2 -1
  179. package/dist/process-liveness.d.ts +2 -0
  180. package/dist/process-liveness.js +61 -17
  181. package/dist/project-delete-ui.d.ts +7 -2
  182. package/dist/project-delete-ui.js +12 -1
  183. package/dist/project-delete.js +8 -1
  184. package/dist/project-knowledge.d.ts +18 -11
  185. package/dist/project-knowledge.js +39 -9
  186. package/dist/project-learning.d.ts +2 -1
  187. package/dist/project-learning.js +15 -45
  188. package/dist/project-memory.d.ts +5 -0
  189. package/dist/project-memory.js +22 -2
  190. package/dist/proof.js +30 -5
  191. package/dist/provider-auth.js +1 -1
  192. package/dist/publish.js +9 -1
  193. package/dist/pull-request-flow.js +5 -0
  194. package/dist/push.d.ts +18 -2
  195. package/dist/push.js +40 -16
  196. package/dist/release-adapters.d.ts +9 -0
  197. package/dist/release-adapters.js +76 -0
  198. package/dist/release-auto.js +13 -13
  199. package/dist/release-card.d.ts +1 -2
  200. package/dist/release-card.js +11 -0
  201. package/dist/release-carry.d.ts +7 -2
  202. package/dist/release-carry.js +39 -9
  203. package/dist/release-cli.d.ts +4 -0
  204. package/dist/release-cli.js +29 -22
  205. package/dist/release-coverage.js +1 -1
  206. package/dist/release-next.d.ts +48 -0
  207. package/dist/release-next.js +161 -0
  208. package/dist/release.d.ts +20 -1
  209. package/dist/release.js +76 -8
  210. package/dist/remote-destination.d.ts +36 -0
  211. package/dist/remote-destination.js +217 -0
  212. package/dist/remote-main.d.ts +48 -0
  213. package/dist/remote-main.js +84 -5
  214. package/dist/repository-context.d.ts +3 -2
  215. package/dist/repository-context.js +112 -70
  216. package/dist/restart-certification.d.ts +1 -0
  217. package/dist/restart-certification.js +8 -6
  218. package/dist/result-acts.d.ts +27 -19
  219. package/dist/result-acts.js +36 -25
  220. package/dist/result-follow-ups.d.ts +2 -2
  221. package/dist/result-follow-ups.js +18 -4
  222. package/dist/result-review.d.ts +1 -1
  223. package/dist/route-change-check.d.ts +4 -0
  224. package/dist/route-change-check.js +13 -0
  225. package/dist/route-change.d.ts +22 -0
  226. package/dist/route-change.js +89 -0
  227. package/dist/route-hooks.js +3 -2
  228. package/dist/route-results.d.ts +6 -1
  229. package/dist/route-results.js +14 -5
  230. package/dist/run-closeout.d.ts +1 -1
  231. package/dist/run-closeout.js +7 -5
  232. package/dist/runner-process.js +3 -2
  233. package/dist/runner.js +2 -1
  234. package/dist/scope.d.ts +6 -1
  235. package/dist/scope.js +22 -9
  236. package/dist/serve.d.ts +1 -1
  237. package/dist/serve.js +24 -18
  238. package/dist/server/chat.js +14 -12
  239. package/dist/server/chrome.d.ts +1 -1
  240. package/dist/server/chrome.js +26 -35
  241. package/dist/server/console.css +23 -0
  242. package/dist/server/guards.js +3 -0
  243. package/dist/server/home.d.ts +6 -0
  244. package/dist/server/home.js +21 -13
  245. package/dist/server/http.js +1 -1
  246. package/dist/server/kit.js +1 -1
  247. package/dist/server/merge-ready-view.d.ts +10 -0
  248. package/dist/server/merge-ready-view.js +30 -0
  249. package/dist/server/pages-assets.d.ts +1 -0
  250. package/dist/server/pages-assets.js +8 -5
  251. package/dist/server/pages.js +103 -82
  252. package/dist/server/render-chat.js +2 -2
  253. package/dist/server/render-code-map.d.ts +30 -0
  254. package/dist/server/render-code-map.js +137 -0
  255. package/dist/server/render-flaky.d.ts +20 -0
  256. package/dist/server/render-flaky.js +59 -0
  257. package/dist/server/render-history.js +1 -1
  258. package/dist/server/render-new-task.d.ts +26 -1
  259. package/dist/server/render-new-task.js +30 -5
  260. package/dist/server/render-pages.d.ts +1 -175
  261. package/dist/server/render-pages.js +38 -313
  262. package/dist/server/render-projects.d.ts +2 -1
  263. package/dist/server/render-projects.js +9 -7
  264. package/dist/server/render-release.d.ts +2 -0
  265. package/dist/server/render-release.js +3 -3
  266. package/dist/server/render-results.d.ts +2 -0
  267. package/dist/server/render-results.js +67 -67
  268. package/dist/server/render-tasks.d.ts +13 -2
  269. package/dist/server/render-tasks.js +153 -136
  270. package/dist/server/retired-routes.d.ts +17 -0
  271. package/dist/server/retired-routes.js +53 -0
  272. package/dist/server/route-table.d.ts +3 -0
  273. package/dist/server/route-table.js +45 -12
  274. package/dist/server/settings.js +129 -22
  275. package/dist/server/task-views.js +61 -26
  276. package/dist/server/tasks.d.ts +2 -6
  277. package/dist/server/tasks.js +174 -321
  278. package/dist/server/welcome.js +1 -1
  279. package/dist/settings-groups-ui.js +1 -1
  280. package/dist/setup-guide.d.ts +2 -2
  281. package/dist/stop-proof.js +6 -4
  282. package/dist/storage-sweep-process.js +1 -1
  283. package/dist/storage-sweep.d.ts +5 -3
  284. package/dist/storage-sweep.js +30 -9
  285. package/dist/storage-ui.js +28 -17
  286. package/dist/storage.d.ts +27 -2
  287. package/dist/storage.js +39 -2
  288. package/dist/store.d.ts +80 -5
  289. package/dist/store.js +296 -87
  290. package/dist/surface.js +45 -4
  291. package/dist/sweep-quarantine.d.ts +1 -0
  292. package/dist/sweep-quarantine.js +6 -4
  293. package/dist/tamper-scan.d.ts +4 -0
  294. package/dist/tamper-scan.js +90 -0
  295. package/dist/task-aliases.d.ts +5 -0
  296. package/dist/task-aliases.js +9 -0
  297. package/dist/task-control.js +4 -1
  298. package/dist/task-outcome-cli.d.ts +2 -0
  299. package/dist/task-outcome-cli.js +17 -17
  300. package/dist/task-sizing.d.ts +5 -0
  301. package/dist/task-sizing.js +2 -0
  302. package/dist/task-source.d.ts +7 -0
  303. package/dist/task-source.js +14 -0
  304. package/dist/task-status.d.ts +9 -0
  305. package/dist/task-status.js +40 -49
  306. package/dist/teardown-git.d.ts +16 -0
  307. package/dist/teardown-git.js +63 -5
  308. package/dist/telegram-progress.js +5 -3
  309. package/dist/telegram-status.js +6 -4
  310. package/dist/test-infra.d.ts +8 -0
  311. package/dist/test-infra.js +18 -0
  312. package/dist/tidy.js +1 -1
  313. package/dist/toolroll-update.js +17 -5
  314. package/dist/tui-contract.d.ts +24 -0
  315. package/dist/tui-source.js +67 -8
  316. package/dist/tui.js +51 -51
  317. package/dist/vanished-retry.d.ts +30 -0
  318. package/dist/vanished-retry.js +174 -0
  319. package/dist/work-index.d.ts +2 -0
  320. package/dist/work-index.js +76 -62
  321. package/dist/work-summary.js +2 -0
  322. package/dist/workspace-ui.d.ts +4 -2
  323. package/dist/workspace-ui.js +28 -22
  324. package/dist/worktree.d.ts +12 -3
  325. package/dist/worktree.js +68 -13
  326. package/package.json +1 -1
  327. package/dist/server/render-next.d.ts +0 -63
  328. package/dist/server/render-next.js +0 -109
@@ -1,6 +1,7 @@
1
1
  import { titleWithin } from "../task-text.js";
2
2
  import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
3
- import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
3
+ import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runCheckStamp, runLiveCommands } from "../store.js";
4
+ import { ranWords } from "../check-stamp.js";
4
5
  import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
5
6
  import { steerCapabilityOf } from "../steer-capabilities.js";
6
7
  import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
@@ -17,7 +18,7 @@ import { DEPRECATED_PAGE } from "../deprecations.js";
17
18
  /** tasks handlers, moved without changing their route bodies. */
18
19
  import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
19
20
  import { createHash, randomBytes, randomUUID } from "node:crypto";
20
- import { lstatSync, realpathSync } from "node:fs";
21
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
21
22
  import {} from "node:http";
22
23
  import { withActor } from "../actor.js";
23
24
  import { gateWords } from "../approval-policy.js";
@@ -32,8 +33,10 @@ import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api
32
33
  import { hasForbiddenControls, validateNote } from "../decision.js";
33
34
  import { builderReadyFor, diagnoseTaskDispatch, firstRunBuilderRepo } from "../dispatch.js";
34
35
  import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
35
- import { newTaskPage } from "./render-new-task.js";
36
- import { nextPage } from "./render-next.js";
36
+ import { fastStartPage, fastTermsRows, importPage, newTaskPage } from "./render-new-task.js";
37
+ import { fileGithubItem, readGithubItem } from "../github-import.js";
38
+ import { allowAutoPublish, autoPublishHeld } from "../release-auto.js";
39
+ import { fastStartBuild, keepGrownChange, prepareFastStart } from "../fast-start.js";
37
40
  import { evidencePack } from "../evidence-pack.js";
38
41
  import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
39
42
  import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
@@ -72,8 +75,9 @@ import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
72
75
  import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
73
76
  import { requestContext } from "./request-context.js";
74
77
  import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
78
+ import { retiredRedirect } from "./retired-routes.js";
75
79
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
76
- import { putAwayPage, boardBody, boardWorkers, inboxFingerprints, inboxPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
80
+ import { putAwayPage, boardBody, boardWorkers, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
77
81
  import { runEfficiencyOf } from "../run-efficiency.js";
78
82
  import { timingFor } from "../latency-timeline.js";
79
83
  import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
@@ -94,7 +98,7 @@ export function createTasksHandlers(runtime) {
94
98
  const showCode = (part, posted = false) => async (ctx) => {
95
99
  const { url, who, response, project } = ctx;
96
100
  if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
97
- return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
101
+ return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/tasks');
98
102
  const actor = { name: who.name, generation: who.session.generation };
99
103
  const sessionId = part === "workspace" || part === "unknown" ? null : /^\/code\/([a-f0-9]{32})/.exec(url.pathname)[1];
100
104
  const read = part === "state" || part === "changes" ? part : null;
@@ -135,7 +139,7 @@ export function createTasksHandlers(runtime) {
135
139
  if (!codingActorAllowed(actor))
136
140
  return read !== null
137
141
  ? respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }))
138
- : refuse(response, who, 403, 'Your access changed. Sign in again.', '/work');
142
+ : refuse(response, who, 403, 'Your access changed. Sign in again.', '/tasks');
139
143
  const message = error instanceof Error ? error.message : 'The coding workspace is unavailable.';
140
144
  if (part === 'ship')
141
145
  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') }));
@@ -144,82 +148,6 @@ export function createTasksHandlers(runtime) {
144
148
  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') }));
145
149
  }
146
150
  };
147
- async function showInbox(ctx) {
148
- const { url, who, request, response, now, project } = ctx;
149
- // With no project open in scoped mode this is the ROLL-UP inbox:
150
- // admission binds inside the bounded queries, every row's repo is
151
- // re-proved here, and rows render as links only (Codex roll-up
152
- // review, findings 6–8). Gaps stay per-project — they are derived
153
- // against one repo's capabilities and roll up dishonestly.
154
- const rollup = project === null && !unscopedMode;
155
- const admission = rollup ? admissionList() : null;
156
- const cancelled = store
157
- .listCancelledBlockersScoped(project, 10, admission)
158
- .filter(one => visible(one.repo) && visible(one.blockerRepo));
159
- const inboxData = {
160
- csrf: who.via === "cookie" ? who.session.csrf : "",
161
- revision: who.via === "cookie" ? who.session.projectRevision : 0,
162
- rollup,
163
- interactive: project !== null,
164
- decisions: store.listDecisionsScoped(project).filter(one => visible(one.repo)).slice(0, 10),
165
- // Each waiting scope wears its consent door (v48 authority repair): a scope whose
166
- // approval is closed — unreadable route, a route that cannot run,
167
- // a pre-routing row whose approval lapsed — reads as needing
168
- // attention, never as something to approve.
169
- approvals: store.scopesAwaitingApproval(project, 10, admission).filter(one => visible(one.repo)).map(one => {
170
- const ref = store.lookupRef(one.taskId);
171
- const scope = store.getScope(one.taskId);
172
- const door = consentDoorOf(scope, routeViewOf(one.taskId, ref, scope, now, who));
173
- return { ...one, closed: door.open ? null : door.title };
174
- }),
175
- requeueables: store.listRequeueablesScoped(project, now, 10, admission).filter(one => visible(one.repo)),
176
- needsVerification: store
177
- .listCompletedWorkScoped(project, 10, admission)
178
- .filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
179
- assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
180
- .map(one => ({
181
- taskId: one.taskId,
182
- title: one.title,
183
- verdict: one.proofVerdict,
184
- repo: one.repo,
185
- matrix: one.proofMatrix,
186
- repairChain: one.runId === null ? null : store.repairChainFor(one.runId),
187
- })),
188
- cancelledBlockers: cancelled,
189
- gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
190
- // The builder step speaks for the same project as Chat's (firstRunBuilderRepo), as dispatch decides.
191
- wizard: withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now)),
192
- worker: (() => {
193
- // The one fact the inbox must never hide (install review): with
194
- // no worker answering, nothing here will ever build, and every
195
- // approval below is a promise nobody is there to keep.
196
- const runners = store.listRunners().filter(one => one.retiredAt === null && (!restricted() || one.repos.some(visible)));
197
- const answering = runners.filter(one => runnerAlive(one, now));
198
- const lastHeard = runners.map(one => one.heartbeatAt).sort().at(-1) ?? null;
199
- return { answering: answering.length, registered: runners.length, lastHeard };
200
- })(),
201
- now,
202
- // Console v2: results ready to review and work running now, from the same admitted work index.
203
- ...(() => {
204
- const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
205
- const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project, root: evidenceRoot });
206
- const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project, root: evidenceRoot });
207
- return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
208
- })(),
209
- };
210
- // Which tabs hold something new since this browser last looked (the dots on a phone).
211
- const tab = parseInboxTab(url.searchParams.get("tab")) ?? "all";
212
- const prints = inboxFingerprints(inboxData);
213
- const seenWords = /(?:^|;\s*)so-inbox-seen=([0-9a-f]{8}(?:\.[0-9a-f]{8}){3})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1]?.split(".") ?? null;
214
- const order = ["needs-you", "ready", "running", "all"];
215
- const seen = Object.fromEntries(order.map((one, index) => [one, seenWords?.[index] ?? ""]));
216
- const unread = order.filter(one => seenWords !== null && seen[one] !== prints[one]);
217
- for (const one of order)
218
- if (tab === "all" || one === tab)
219
- seen[one] = prints[one];
220
- response.setHeader("Set-Cookie", `so-inbox-seen=${order.map(one => seen[one] || prints[one]).join(".")}; SameSite=Lax; Path=/; Max-Age=31536000`);
221
- return sendScreen(response, 200, inboxPage(chromeFor(project, "inbox"), { ...inboxData, tab, unread }));
222
- }
223
151
  async function showWork(ctx) {
224
152
  const { url, who, response, now, project } = ctx;
225
153
  // The Work destination (workspace package 1): every task in view as
@@ -232,7 +160,7 @@ export function createTasksHandlers(runtime) {
232
160
  // The stored-state filter the old task list had (`?state=`): an unknown state is refused, as it was there.
233
161
  const wantedState = url.searchParams.get("state");
234
162
  if (wantedState !== null && !TASK_STATES.includes(wantedState))
235
- return refuse(response, who, 400, "no such state", "/work");
163
+ return refuse(response, who, 400, "no such state", "/tasks");
236
164
  const state = wantedState;
237
165
  const rollup = project === null && !unscopedMode;
238
166
  let work;
@@ -253,7 +181,7 @@ export function createTasksHandlers(runtime) {
253
181
  }
254
182
  catch (error) {
255
183
  if (error instanceof WorkIndexCursorError)
256
- return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
184
+ return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/tasks');
257
185
  throw error;
258
186
  }
259
187
  // A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
@@ -305,179 +233,13 @@ export function createTasksHandlers(runtime) {
305
233
  });
306
234
  // One project's Tasks dock that project's own conversation (v77).
307
235
  const projectThread = url.searchParams.has('project') && project !== null && managedRepos().includes(project) ? project : null;
308
- const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
236
+ const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/tasks?project=${encodeURIComponent(projectThread)}`);
309
237
  if (docked !== null)
310
238
  page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
311
239
  // Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
312
240
  page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
313
241
  return sendScreen(response, 200, page);
314
242
  }
315
- async function showNext(ctx) {
316
- const { url, who, response, now, project } = ctx;
317
- // Triage: everything waiting on a person, one at a time, hardest-
318
- // blocked first — oldest question, then plans and scopes to approve,
319
- // then stalled work to retry, then requirement gaps. `skip` is a
320
- // bounded, session-free cursor of keys the operator set aside; every
321
- // act 303s back here, which is what makes it a flow and not a list.
322
- const skipped = new Set((url.searchParams.get("skip") ?? "").split(",").filter(one => /^[darg]:[A-Za-z0-9._:-]{1,80}$/.test(one)).slice(0, 20));
323
- const decisions = store.listDecisionsScoped(project).filter(one => one.state !== "answered");
324
- const approvals = store.scopesAwaitingApproval(project, 20);
325
- const requeueables = store.listRequeueablesScoped(project, now, 20);
326
- const gaps = project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0);
327
- const queue = [
328
- ...decisions.map(decision => ({ key: `d:${decision.id}`, kind: "decision", decision })),
329
- ...approvals.map(approval => ({ key: `a:${approval.taskId}`, kind: "approval", approval })),
330
- ...requeueables.map(stalled => ({ key: `r:${stalled.taskId}`, kind: "requeue", stalled })),
331
- ...gaps.map(gap => ({ key: `g:${gap.key.replace(/[^A-Za-z0-9._:-]/g, "_")}`, kind: "gap", gap })),
332
- ];
333
- const remaining = queue.filter(one => !skipped.has(one.key));
334
- const item = remaining[0] ?? null;
335
- const csrf = who.via === "cookie" ? who.session.csrf : "";
336
- if (item !== null && item.kind === "approval") {
337
- // The card restates the digest-bound terms, so the nonce may be
338
- // minted here — same rule as the task screen, same binding.
339
- const ref = store.lookupRef(item.approval.taskId);
340
- const scope = store.getScope(item.approval.taskId);
341
- const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
342
- const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
343
- // The same agents block the task page and chat sign under (v48),
344
- // and the same consent door: closed, it mints no nonce.
345
- const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
346
- const nonce = who.via === "cookie" && consentDoorOf(scope, route).open ? mintApprovalNonce(who.name, item.approval.taskId, approvalDigest) : "";
347
- return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
348
- item, scope,
349
- planDocument: planView?.document ?? null,
350
- planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
351
- approvalDigest,
352
- deliverable: ref?.deliverable ?? "branch",
353
- route,
354
- origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
355
- // Whether a remembered password may approve these exact terms: the task page's own preview (task-views.ts
356
- // approvalFresh), read without minting a second nonce. Display only; the approve POST proves it again.
357
- remembered: nonce !== "" && runtime.taskViewData(item.approval.taskId, who, null, { mintNonce: false })?.approvalFresh != null,
358
- csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
359
- }));
360
- }
361
- return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
362
- item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
363
- approvalDigest: null, route: null,
364
- remaining: remaining.length, skipped: [...skipped], now,
365
- }));
366
- }
367
- async function showBoard(ctx) {
368
- const { url, who, response, now, project } = ctx;
369
- // scope=all is the rolled-up view: every project this server was
370
- // allowed to serve, on one board. The ceiling still rules row by row
371
- // (rowVisible, the same predicate as every list) — a repo outside
372
- // the server's configuration never renders a card, whatever the
373
- // database holds. Unplaced work (repo NULL) appears: it dispatches
374
- // anywhere, so every board honestly owns it.
375
- const all = url.searchParams.get("scope") === "all";
376
- // Roll-up admission happens BEFORE the query limit (Codex round 2,
377
- // finding 11) — and root ceilings enumerate too, through the STORED
378
- // repos that pass the ceiling (attended review, finding 3); the
379
- // per-row visible() re-check below stays either way.
380
- const admission = all ? admissionList() : null;
381
- const snapshot = store.boardScoped(all ? null : project, now, 200, admission);
382
- const admitted = all
383
- ? snapshot.tasks.filter(facts => facts.repo === null || visible(facts.repo))
384
- : snapshot.tasks;
385
- const done = all
386
- ? snapshot.done.filter(row => row.repo === null || visible(row.repo))
387
- : snapshot.done;
388
- // A blocker may live in a repo this server must not speak about —
389
- // redact its state before the pure classifier composes a sentence
390
- // from it (Codex round 2, finding 12). The dependency's NAME stays:
391
- // the edge belongs to the visible task; the other project's live
392
- // status does not.
393
- const cards = admitted.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo)
394
- ? { ...facts, blockerState: null }
395
- : facts, now));
396
- // "Since you last looked": what concluded between this session's
397
- // previous full board read and now. Fragment polls never move the
398
- // anchor — an open tab is not a person looking.
399
- let delta = null;
400
- if (who.via === "cookie" && url.searchParams.get("fragment") !== "1") {
401
- const prev = who.session.sawBoardAt;
402
- if (prev !== null && now.getTime() - prev > 5 * 60_000) {
403
- const sinceIso = new Date(prev).toISOString();
404
- const runs = store.runsSinceScoped(sinceIso, all ? null : project).filter(one => all ? one.taskId !== "" : true);
405
- delta = {
406
- agoMinutes: Math.round((now.getTime() - prev) / 60_000),
407
- built: runs.filter(one => one.outcome === "built" || one.outcome === "no-change").length,
408
- failed: runs.filter(one => one.outcome === "failed").length,
409
- questions: store.listDecisionsScoped(all ? null : project).filter(one => one.createdAt >= sinceIso && one.state !== "answered").length,
410
- };
411
- if (delta.built === 0 && delta.failed === 0 && delta.questions === 0)
412
- delta = null;
413
- }
414
- who.session.sawBoardAt = now.getTime();
415
- }
416
- // A completed task whose proof is short or refuted and not yet
417
- // accepted (Priority 2) reads "needs verification", not done — the
418
- // board's once-and-only-once rule holds because this split is the
419
- // ONE place a done row becomes either lane; the done-lane render
420
- // below never sees the rows filtered out here.
421
- const unverifiedDone = done.filter(row => (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted);
422
- const verifiedDone = done.filter(row => !unverifiedDone.includes(row));
423
- const unverifiedCards = unverifiedDone.map(row => {
424
- // v40: the SAME chip gains one more word when a repair chain
425
- // exists for this row's own run — never a second card.
426
- const chain = row.runId === null ? null : store.repairChainFor(row.runId);
427
- const repairChain = chain === null
428
- ? null
429
- : {
430
- attempt: chain.attempt,
431
- outcome: chain.outcome,
432
- approved: chain.draftTask !== null && (store.getScope(chain.draftTask)?.approvedAt ?? null) !== null,
433
- };
434
- return attentionCardForUnverifiedDone({
435
- taskId: row.taskId,
436
- title: row.title,
437
- repo: row.repo,
438
- completedAt: row.completedAt,
439
- proofVerdict: row.proofVerdict,
440
- proofMatrix: row.proofMatrix,
441
- repairChain,
442
- });
443
- });
444
- // Project-level Needs you (evals-nightly.md, part A3): GitHub's nightly lane failures and the plane's own nightly
445
- // check of main, for the projects this board shows.
446
- const nightlyCards = nightlyNeedsYou(store, repo => visible(repo) && (all || project === null || repo === project)).map(need => ({
447
- lane: "attention", taskId: need.key, title: need.title, repo: need.repo, reason: need.reason, href: need.href, claim: null,
448
- stalledSince: need.since, attempt: null, overdue: false, priority: 0, assignedRunner: null,
449
- }));
450
- const laneCards = [...cards, ...unverifiedCards, ...nightlyCards];
451
- // The worker strip: the workers this board's work can go to — bound to
452
- // this project, or (rolled up) to any project the ceiling admits.
453
- const workers = boardWorkers(store.listRunners(), { all, project, visible });
454
- const body = boardBody({ cards: laneCards, workers, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
455
- if (url.searchParams.get("fragment") === "1") {
456
- // The live region alone — the in-page swapper's diet. Same auth,
457
- // same ceiling, no shell, no scripts (finding 2).
458
- return respond(response, 200, "text/html; charset=utf-8", body);
459
- }
460
- if (url.searchParams.get("view") === "order") {
461
- // The board's ORDER view (operator request): the same screen, flipped
462
- // to dispatch order with the queue's drag handles. Reordering and
463
- // reserving are scheduling, not authority, so this is the one view
464
- // where a drag does anything; the state view stays a view.
465
- const csrf = who.via === "cookie" ? who.session.csrf : "";
466
- const revision = who.via === "cookie" ? who.session.projectRevision : 0;
467
- const region = queueRegionFor(project, csrf, revision, now);
468
- return sendScreen(response, 200, screen("board", joinHtml([
469
- html `<h1>Board</h1>`,
470
- 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>`,
471
- html `<div id="queue-region">${region}</div>`,
472
- html `<p class="meta" id="queue-region-stamp"></p>`,
473
- ], "\n"), { chrome: chromeFor(project, "board"), functional: { script: queueScript(), fetches: true } }));
474
- }
475
- const regionBody = html `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
476
- return sendScreen(response, 200, screen("board", regionBody, {
477
- chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
478
- functional: { script: regionScript("board-region", "1", undefined, { animate: true }) + boardTickScript(), fetches: true },
479
- }));
480
- }
481
243
  /** A completed (or attempted) row as the review list ranks it: its proof reasons, CI standing, and the assignment
482
244
  * when that assignment's receipt is this very row. */
483
245
  function reviewRowOf(row, now) {
@@ -574,9 +336,9 @@ export function createTasksHandlers(runtime) {
574
336
  const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
575
337
  // An exact link to a result this person can't see (or that never existed) reads the same either way.
576
338
  if (expectedRun !== null && wantedId !== null && chosen === null)
577
- return refuse(response, who, 404, "No such result in your projects.", "/work");
339
+ return refuse(response, who, 404, "No such result in your projects.", "/tasks");
578
340
  if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
579
- 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") }));
341
+ 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="/tasks?view=ready">Review results</a></p>`, { chrome: chromeFor(project, "runs") }));
580
342
  }
581
343
  const selected = picked !== null ? picked.selected : selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
582
344
  // Opening a run's own result page is viewing it: the shell says so with a keyed POST /agents/seen (easy-start N1:
@@ -607,39 +369,59 @@ export function createTasksHandlers(runtime) {
607
369
  reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
608
370
  return sendScreen(response, 200, reviewPage);
609
371
  }
610
- /** Finished tasks live in the Tasks list's Finished view now (UI revamp wave B): the old address keeps answering, as a
611
- * GET alias, with a named project carried along. */
612
- async function showDone(ctx) {
613
- const { url, response } = ctx;
614
- const named = url.searchParams.get("project");
615
- return redirectGet(response, `/work?view=completed${named === null || named === "" ? "" : `&project=${encodeURIComponent(named)}`}`);
616
- }
617
372
  /** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
618
373
  * redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
619
374
  * goes to the add page, which now pre-fills it. Anything else is dropped. */
375
+ /** Tasks (docs/plans/plain-toolroll.md P1.4): the one list, with List, Board, Ready, Archived and an every-project
376
+ * scope (`scope=all`). `view=board` is the Board, `view=order` the queue's order, `view=done` Finished. */
620
377
  async function showTasks(ctx) {
621
- const { url, who, response } = ctx;
622
- const wanted = url.searchParams.get("state");
623
- if (wanted !== null && !TASK_STATES.includes(wanted))
624
- return refuse(response, who, 400, "no such state", "/work");
378
+ const { url } = ctx;
625
379
  const template = url.searchParams.get("template");
626
380
  if (template !== null)
627
- return redirect(response, `/tasks/new?template=${encodeURIComponent(template)}`);
628
- return redirect(response, wanted === null ? "/work" : `/work?state=${encodeURIComponent(wanted)}`);
381
+ return redirect(ctx.response, `/tasks/new?template=${encodeURIComponent(template)}`);
382
+ const view = url.searchParams.get("view");
383
+ if (view === "order")
384
+ return showOrder(ctx);
385
+ // The list reads its own keys: Board is its layout, every project its roll-up, Finished its completed view.
386
+ const read = new URL(url.href);
387
+ if (view === "board") {
388
+ read.searchParams.delete("view");
389
+ read.searchParams.set("layout", "board");
390
+ }
391
+ if (view === "done")
392
+ read.searchParams.set("view", "completed");
393
+ if (read.searchParams.get("scope") === "all") {
394
+ read.searchParams.delete("scope");
395
+ read.searchParams.set("all", "1");
396
+ }
397
+ return showWork({ ...ctx, url: read });
629
398
  }
630
- async function showQueue(ctx) {
399
+ /** The queue's order view, under Tasks: dispatch order with drag handles (scheduling, never authority). */
400
+ async function showOrder(ctx) {
631
401
  const { url, who, response, project } = ctx;
632
402
  const csrf = who.via === "cookie" ? who.session.csrf : "";
633
403
  const revision = who.via === "cookie" ? who.session.projectRevision : 0;
634
- // Column headers read one thing beyond the queue snapshot: live claims
635
- // in THIS project, per worker (the capacity is global), so the header
636
- // names both and never a ratio.
637
- if (url.searchParams.get("fragment") === "1") {
404
+ if (url.searchParams.get("fragment") === "1")
638
405
  return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
406
+ const region = queueRegionFor(project, csrf, revision, clock());
407
+ return sendScreen(response, 200, screen("order", joinHtml([
408
+ html `<h1>Order the queue</h1>`,
409
+ html `<p class="meta board-view"><a href="/tasks?view=board">Board</a> · <strong>order</strong> <span class="meta">— drag to reorder, or onto a builder to keep it for that builder</span></p>`,
410
+ html `<div id="queue-region">${region}</div>`,
411
+ html `<p class="meta" id="queue-region-stamp"></p>`,
412
+ ], "\n"), { chrome: chromeFor(project, "work"), functional: { script: queueScript(), fetches: true } }));
413
+ }
414
+ /** /n/<number>: a push's task link, by the task's stored number, never its title. It opens the task's page only
415
+ * inside the viewer's own projects; anything else is the same not-found as an unknown number. */
416
+ async function showNumberedTask(ctx) {
417
+ const { url, who, response } = ctx;
418
+ const number = Number(url.pathname.slice("/n/".length));
419
+ const ref = Number.isSafeInteger(number) ? store.refForId(number) : null;
420
+ const task = ref === null || ref.backend !== "built-in" ? null : ref.externalId;
421
+ if (task === null || ref === null || ref.repo === null || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo)) {
422
+ return refuse(response, who, 404, "There's no page at this address.", "/tasks");
639
423
  }
640
- // The queue is the board's order view (reduction pass §1): the URL
641
- // keeps answering, as a redirect, so nothing anyone bookmarked 404s.
642
- return redirect(response, QUEUE_VIEW);
424
+ return redirectGet(response, taskHref(task));
643
425
  }
644
426
  async function showNewTask(ctx) {
645
427
  const picked = (() => { const name = ctx.url.searchParams.get("template"); return name === null ? null : templateByName(name); })();
@@ -657,7 +439,7 @@ export function createTasksHandlers(runtime) {
657
439
  return noPage(who, response);
658
440
  const family = familyOf(task.taskId);
659
441
  if (family === null)
660
- return refuse(response, who, 404, "no such task", "/work");
442
+ return refuse(response, who, 404, "no such task", "/tasks");
661
443
  const version = url.searchParams.get("version");
662
444
  if (version !== null && !family.versions.some(one => one.id === version))
663
445
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
@@ -709,12 +491,12 @@ export function createTasksHandlers(runtime) {
709
491
  const access = workAccess();
710
492
  const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
711
493
  if (family === null || !visible(family.root.repo))
712
- return refuse(response, who, 404, "no such task", "/work");
494
+ return refuse(response, who, 404, "no such task", "/tasks");
713
495
  if (evidence.taskId !== family.root.id)
714
496
  return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
715
497
  const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
716
498
  if (pack === null)
717
- return refuse(response, who, 404, "no such task", "/work");
499
+ return refuse(response, who, 404, "no such task", "/tasks");
718
500
  if (url.searchParams.get("format") === "json") {
719
501
  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"`,
720
502
  "cache-control": "no-store", "x-content-type-options": "nosniff" });
@@ -860,7 +642,9 @@ export function createTasksHandlers(runtime) {
860
642
  who.via === "cookie" &&
861
643
  who.session.editorLinks === true &&
862
644
  // A reviewer run (v29) never had a checkout — no files to open.
863
- found.worktree !== null
645
+ found.worktree !== null &&
646
+ // Idle cleanup removes the folder and a later run makes it again: no link to a folder that isn't there.
647
+ existsSync(found.worktree)
864
648
  ? { worktree: found.worktree }
865
649
  : null, options.editorLinks !== undefined &&
866
650
  options.localRunner !== undefined &&
@@ -1067,6 +851,30 @@ export function createTasksHandlers(runtime) {
1067
851
  }
1068
852
  // One filing door for every surface (Codex adoption review, finding 7).
1069
853
  const after = (body.get("after") ?? "").trim();
854
+ // From an issue or pull request (github-import.ts): read and quoted first; filing is the second press, on what was read.
855
+ const fromLink = (body.get("from") ?? "").trim();
856
+ if (fromLink !== "") {
857
+ if (who.via !== "cookie" || repo === "")
858
+ return newTaskScreen(ctx, 400, "Choose the project this issue belongs to.", draft);
859
+ if (body.get("from-read") !== "1") {
860
+ const read = await readGithubItem(store, { url: fromLink, repo });
861
+ if (!read.ok)
862
+ return newTaskScreen(ctx, 400, read.message, draft);
863
+ return sendScreen(response, 200, importPage(chromeFor(project ?? null, "tasks"), { repo, url: read.item.url, title: read.item.title, body: read.item.body, kind: read.item.kind, number: read.item.number, head: read.item.head }));
864
+ }
865
+ const filed = await fileGithubItem(store, { url: fromLink, repo, actor: who.name, shownHead: body.get("from-head") ?? null }, now);
866
+ if (!filed.ok)
867
+ return newTaskScreen(ctx, filed.reason === "head-moved" ? 409 : 400, filed.message, draft);
868
+ return redirect(response, taskHref(filed.taskId));
869
+ }
870
+ // Small work a person starts themselves: their words become the exact terms, and Build is their yes (fast-start.ts).
871
+ // Anything else, or any doubt, is the ordinary filing below. `plain` is "Plan it first" from that form.
872
+ if (who.via === "cookie" && body.get("plain") !== "1" && repo !== "" && goal === "" && notThis === "" && touchesGiven.length === 0 && !scout && id === "" && after === ""
873
+ && (qualityMode === null || qualityMode === "default") && (permissionMode === null || permissionMode === "auto")) {
874
+ const fast = await prepareFastStart(store, { actor: who.name, repo, title: typed.trim(), via: "console", ...(permissionMode === null ? {} : { permissionMode }), password: fastPasswordFacts(who) }, now);
875
+ if (fast.eligible)
876
+ return sendScreen(response, 200, fastStartPage(chromeFor(project ?? null, "tasks"), { title: typed.trim(), repo, preview: fast.preview, digest: fast.digest, terms: fastTermsRows(store, fast.scope, repo) }));
877
+ }
1070
878
  let chainProblem = null;
1071
879
  const made = store.transact(() => {
1072
880
  // A project folder's identity changed while this request waited (a root or link now leads elsewhere): what was
@@ -1246,7 +1054,7 @@ export function createTasksHandlers(runtime) {
1246
1054
  const id = Number(answer[1]);
1247
1055
  const requestedReturn = body.get("return");
1248
1056
  const decisionBack = requestedReturn === "next"
1249
- ? "/next"
1057
+ ? "/"
1250
1058
  : requestedReturn === null
1251
1059
  ? `/d/${id}`
1252
1060
  : safeChatReturn(requestedReturn);
@@ -1300,7 +1108,7 @@ export function createTasksHandlers(runtime) {
1300
1108
  const back = taskHref(act.taskId);
1301
1109
  const ref = store.lookupRef(act.taskId);
1302
1110
  if (ref === null || !visible(ref.repo))
1303
- return refuse(response, who, 404, "no such task", "/work");
1111
+ return refuse(response, who, 404, "no such task", "/tasks");
1304
1112
  const named = (body.get("run") ?? "").trim();
1305
1113
  const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1306
1114
  const runTask = run === null ? null : store.externalIdFor(run.taskRef);
@@ -1335,7 +1143,7 @@ export function createTasksHandlers(runtime) {
1335
1143
  return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
1336
1144
  const ref = store.lookupRef(act.taskId);
1337
1145
  if (ref === null || !visible(ref.repo))
1338
- return refuse(response, who, 404, "no such task", "/work");
1146
+ return refuse(response, who, 404, "no such task", "/tasks");
1339
1147
  const named = (body.get("run") ?? "").trim();
1340
1148
  const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
1341
1149
  const runTask = run === null ? null : store.externalIdFor(run.taskRef);
@@ -1697,7 +1505,7 @@ export function createTasksHandlers(runtime) {
1697
1505
  const resolved = store.resolveIncident(id, who.name, now);
1698
1506
  if (!resolved)
1699
1507
  return refuse(response, who, 409, "already resolved, or never open");
1700
- const back = body.get("return") === "inbox" ? "/inbox" : openRow === undefined ? "/" : taskHref(openRow.taskId);
1508
+ const back = body.get("return") === "inbox" ? "/" : openRow === undefined ? "/" : taskHref(openRow.taskId);
1701
1509
  return redirect(response, back);
1702
1510
  }
1703
1511
  /** The compact recent-runs list for the master pane. */
@@ -1823,10 +1631,10 @@ export function createTasksHandlers(runtime) {
1823
1631
  function taskMutation(response, who, taskId, verb, posted, now) {
1824
1632
  const ref = store.lookupRef(taskId);
1825
1633
  if (ref === null || store.getTask(taskId) === null) {
1826
- return refuse(response, who, 404, "no such task", "/work");
1634
+ return refuse(response, who, 404, "no such task", "/tasks");
1827
1635
  }
1828
1636
  if (!visible(ref.repo)) {
1829
- return refuse(response, who, 404, "no such task", "/work");
1637
+ return refuse(response, who, 404, "no such task", "/tasks");
1830
1638
  }
1831
1639
  switch (verb) {
1832
1640
  case "steer": {
@@ -2091,7 +1899,7 @@ export function createTasksHandlers(runtime) {
2091
1899
  return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
2092
1900
  }
2093
1901
  // Allow-listed return only — never an arbitrary URL from the form.
2094
- return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
1902
+ return redirect(response, body.get("return") === "inbox" || body.get("return") === "next" ? "/" : taskHref(taskId));
2095
1903
  }
2096
1904
  case "follow-up": {
2097
1905
  const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
@@ -2459,7 +2267,7 @@ export function createTasksHandlers(runtime) {
2459
2267
  const body = readForm(posted, CONSOLE_FORMS.taskApprove);
2460
2268
  const requestedReturn = body.get("return");
2461
2269
  const approvalBack = requestedReturn === "next"
2462
- ? "/next"
2270
+ ? "/"
2463
2271
  : requestedReturn === null
2464
2272
  ? taskHref(taskId)
2465
2273
  : safeChatReturn(requestedReturn);
@@ -2960,6 +2768,8 @@ export function createTasksHandlers(runtime) {
2960
2768
  timing: timingFor(store, run.id, row.taskId),
2961
2769
  // How far its checks reached (check-access.ts), display only.
2962
2770
  checkAccess: store.runCheckAccess(run.id),
2771
+ // How its checks actually ran, by what was stamped at launch: said only for a result whose checks ran.
2772
+ checkRan: store.artifactsFor(run.id).some(one => one.kind === "check-log") ? ranWords(runCheckStamp(store, run.id)) : null,
2963
2773
  reviewRetry: store.reviewRetryStateOf(run.id),
2964
2774
  review: reviewFactsFor(run.id),
2965
2775
  notes: store.notesForRun(run.id),
@@ -3119,14 +2929,17 @@ export function createTasksHandlers(runtime) {
3119
2929
  "code.ship-preview": showCode("ship", true),
3120
2930
  "code.other": showCode("unknown"),
3121
2931
  // Retired into Tasks and the task page (src/design/KIT.md): each old address still answers, as a redirect.
3122
- "inbox": showInbox,
3123
- "work": showWork,
3124
- "next": showNext,
3125
- "board": showBoard,
3126
- "review": showReview,
3127
- "done": async (ctx) => { const to = retiredTasksAddress("done", ctx.url); return to === null ? showDone(ctx) : redirect(ctx.response, to); },
2932
+ // Retired pages (docs/plans/plain-toolroll.md P1.4): permanent redirects, no page handler of their own.
2933
+ "inbox": async (ctx) => retiredRedirect("inbox", ctx.url, ctx.response),
2934
+ "work": async (ctx) => retiredRedirect("work", ctx.url, ctx.response),
2935
+ "next": async (ctx) => retiredRedirect("next", ctx.url, ctx.response),
2936
+ "board": async (ctx) => retiredRedirect("board", ctx.url, ctx.response),
2937
+ // A result keeps its own page; the bare list moved to Tasks' Ready view.
2938
+ "review": async (ctx) => ctx.url.searchParams.has("result") ? showReview(ctx) : retiredRedirect("review", ctx.url, ctx.response),
2939
+ "done": async (ctx) => retiredRedirect("done", ctx.url, ctx.response),
3128
2940
  "tasks": showTasks,
3129
- "queue": showQueue,
2941
+ "queue": async (ctx) => retiredRedirect("queue", ctx.url, ctx.response),
2942
+ "n.task": showNumberedTask,
3130
2943
  "tasks.new": showNewTask,
3131
2944
  "task.page": showTask,
3132
2945
  "task.evidence": showTaskEvidence,
@@ -3147,6 +2960,8 @@ export function createTasksHandlers(runtime) {
3147
2960
  "code.answer-send": codeAct("answer"),
3148
2961
  "code.act-other": codeAct("unknown"),
3149
2962
  "tasks.add": addTask,
2963
+ "tasks.fast": buildNow,
2964
+ "task.act.keep-grown": keepGrown,
3150
2965
  "queue.move": queueMove,
3151
2966
  "queue.note": queueNote,
3152
2967
  "decision.answer": answerDecision,
@@ -3208,7 +3023,44 @@ export function createTasksHandlers(runtime) {
3208
3023
  const startsOn = draft?.values?.get("repo") ?? project ?? chrome.projects?.[0]?.path ?? null;
3209
3024
  const found = startsOn === null ? null : runtime.firstTasks.get(startsOn)?.found ?? null;
3210
3025
  const ideas = found ?? (runtime.firstRunStepsNow(ctx.now) !== null && !store.hasAnyWork() ? firstTaskSuggestions({ issues: [], todos: [] }) : []);
3211
- return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas));
3026
+ return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas, ctx.url.searchParams.get("from")?.slice(0, 300) ?? null));
3027
+ }
3028
+ /** What the password rule reads for this request (approval-password.ts), the same facts the approve forms read. */
3029
+ function fastPasswordFacts(who) {
3030
+ return { passwordless: passwordlessSessionFor(store, who.name), localOwner: who.via === "cookie" && who.authority === "local-owner",
3031
+ freshAt: withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true))?.at ?? null, returnTo: "/tasks/new" };
3032
+ }
3033
+ /** Build: the fast start's one write. Every check is made again inside fastStartBuild's transaction. */
3034
+ async function buildNow(ctx) {
3035
+ const { who, response, now, posted } = ctx;
3036
+ const body = readForm(posted, CONSOLE_FORMS.tasksFast);
3037
+ if (who.via !== "cookie")
3038
+ return refuse(response, who, 403, "Only a person starts a build this way.", "/tasks/new");
3039
+ const preview = body.get("preview") ?? "";
3040
+ const row = store.handle.prepare("SELECT repo, title FROM fast_preview WHERE id = ?").get(preview);
3041
+ const title = row === undefined ? "" : String(row["title"]);
3042
+ if (row !== undefined && !visible(String(row["repo"])))
3043
+ return refuse(response, who, 403, "That project is outside your access.", "/projects");
3044
+ const built = spendingOnlyOnSuccess(() => withFreshness("approve-standard", () => fastStartBuild(store, { preview, digest: body.get("digest") ?? "", actor: who.name, via: "console", password: fastPasswordFacts(who),
3045
+ approveNow: (taskId, sawDigest, fast) => approveScope(store, taskId, who.name, now, sawDigest, "", {}, fast) }, now)), result => !result.ok);
3046
+ if (!built.ok)
3047
+ return newTaskScreen(ctx, built.reason === "changed" ? 409 : 403, built.message, { title, goal: "", not: "", touches: "", acceptance: "" });
3048
+ const actionContext = requestContext.getStore();
3049
+ if (actionContext !== undefined)
3050
+ actionContext.createdTask = built.taskId;
3051
+ return redirect(response, taskHref(built.taskId));
3052
+ }
3053
+ /** "Approve it to keep it": a fast start that grew gets a plan for its exact commit, on the ordinary approval. It seals nothing. */
3054
+ async function keepGrown(ctx) {
3055
+ const { url, who, response, now, posted } = ctx;
3056
+ readForm(posted, CONSOLE_FORMS.keepGrown);
3057
+ const act = matchTaskPath(url.pathname, "/keep-grown$");
3058
+ if (act === null)
3059
+ return noPage(who, response);
3060
+ const kept = keepGrownChange(store, act.taskId, now);
3061
+ if (!kept.ok)
3062
+ return taskScreen(response, who, act.taskId, kept.message, 409);
3063
+ return redirect(response, taskHref(act.taskId));
3212
3064
  }
3213
3065
  return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3214
3066
  }
@@ -3221,7 +3073,7 @@ export function createTidyHandlers(runtime) {
3221
3073
  const { url, who, response, now, project } = ctx;
3222
3074
  const days = tidyDays(url.searchParams.get("days"));
3223
3075
  if (days === null)
3224
- return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/work/put-away");
3076
+ return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/tasks/archive");
3225
3077
  const releases = releaseFactsOf(store);
3226
3078
  const items = tidyPreview(store, now, { releases, days, access: workAccess() });
3227
3079
  return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
@@ -3231,22 +3083,22 @@ export function createTidyHandlers(runtime) {
3231
3083
  const { who, response, now, posted } = ctx;
3232
3084
  const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
3233
3085
  const days = tidyDays(body.get("days"));
3234
- const back = (problem) => redirect(response, `/work/put-away?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
3086
+ const back = (problem) => redirect(response, `/tasks/archive?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
3235
3087
  if (days === null)
3236
3088
  return back("Choose a number of days from 1 to 365.");
3237
3089
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
3238
- return back("Enter your Toolroll password to put these away.");
3090
+ return back("Enter your Toolroll password to archive these.");
3239
3091
  const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
3240
3092
  if (!done.ok)
3241
3093
  return back(done.message);
3242
3094
  bustBadge();
3243
- return redirect(response, "/work?view=archived");
3095
+ return redirect(response, "/tasks?view=archived");
3244
3096
  }
3245
3097
  const oneTask = (kind) => async (ctx) => {
3246
3098
  const { url, who, response, now, posted } = ctx;
3247
3099
  const act = matchTaskPath(url.pathname, `/${kind}$`);
3248
3100
  if (act === null)
3249
- return refuse(response, who, 404, "There's no page at this address.", "/work");
3101
+ return refuse(response, who, 404, "There's no page at this address.", "/tasks");
3250
3102
  const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
3251
3103
  const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
3252
3104
  const done = kind === "archive"
@@ -3258,30 +3110,14 @@ export function createTidyHandlers(runtime) {
3258
3110
  const to = safeReturn(body.get("return"));
3259
3111
  return redirect(response, to === "/" ? taskHref(act.taskId) : to);
3260
3112
  };
3261
- return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
3113
+ return { "tasks.archive": showPutAway, "work.put-away": async (ctx) => retiredRedirect("work.put-away", ctx.url, ctx.response),
3114
+ "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
3262
3115
  }
3263
3116
  /** A retired page's GET alias: a plain 302 (the shared 303 helper belongs to POST landings), as pages.ts morningAlias. */
3264
3117
  function redirectGet(response, to) {
3265
3118
  response.writeHead(302, { ...SAFETY, Location: to });
3266
3119
  response.end();
3267
3120
  }
3268
- /**
3269
- * Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
3270
- * Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
3271
- * address still answers itself.
3272
- */
3273
- export function retiredTasksAddress(route, url) {
3274
- // (The Board keeps its own page for now: Tasks' Board view is where the app links; see the wave A report.)
3275
- const project = url.searchParams.get("project");
3276
- const keep = project === null || url.searchParams.getAll("project").length !== 1 ? "" : `project=${encodeURIComponent(project)}&`;
3277
- switch (route) {
3278
- case "done": return `/work?${keep}view=completed`;
3279
- case "board":
3280
- if (url.searchParams.get("view") === "order" || url.searchParams.get("fragment") === "1")
3281
- return null;
3282
- return url.searchParams.get("scope") === "all" ? "/work?all=1&layout=board" : `/work?${keep}layout=board`;
3283
- }
3284
- }
3285
3121
  /** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
3286
3122
  export const BOARD_COLUMN_LIMIT = 40;
3287
3123
  /**
@@ -3336,7 +3172,7 @@ export function createAgentSignalHandlers(runtime) {
3336
3172
  * unchanged approve() (release-approval.ts approveReleaseCard).
3337
3173
  */
3338
3174
  export function createReleaseSheetHandlers(runtime) {
3339
- const { store, sendScreen, chromeFor, visible, mintApprovalNonce, clock } = runtime;
3175
+ const { store, sendScreen, chromeFor, visible, mintApprovalNonce, clock, authenticateApprover } = runtime;
3340
3176
  const missing = (who, response) => refuse(response, who, 404, "There's no release waiting for approval at this address.", "/work");
3341
3177
  /** The sheet, with a problem said where it happened. */
3342
3178
  function sheetScreen(ctx, taskId, problem, status) {
@@ -3361,7 +3197,7 @@ export function createReleaseSheetHandlers(runtime) {
3361
3197
  const gate = store.approvalGate(taskId, who.name, "person");
3362
3198
  return freshlyApprovable(store, taskId, card.scope, gate.verdict === "seal" && !gate.protectedWork);
3363
3199
  })();
3364
- return sendScreen(response, status, releaseSheetPage(chromeFor(project ?? ref.repo, "work"), { card, nonce, facts: approvalFactsNow({ remembered }), local, problem }));
3200
+ return sendScreen(response, status, releaseSheetPage(chromeFor(project ?? ref.repo, "work"), { card, nonce, facts: approvalFactsNow({ remembered }), local, problem, held: who.role === "approver" ? autoPublishHeld(store, taskId) : null }));
3365
3201
  }
3366
3202
  async function showSheet(ctx) {
3367
3203
  const task = matchTaskPath(ctx.url.pathname, "/release$");
@@ -3394,5 +3230,22 @@ export function createReleaseSheetHandlers(runtime) {
3394
3230
  return sheetScreen(ctx, taskId, refused.message, 403);
3395
3231
  return sheetScreen(ctx, taskId, approveRefusalWords(refused.reason), refused.reason === "changed" || refused.reason === "unrouted" ? 409 : 403);
3396
3232
  }
3397
- return { routes: { "task.release": showSheet, "release.locate": locate }, sheetScreen, approveFromSheet };
3233
+ /** A held automatic release goes on: a person only, proved now. The watch then resumes it (release-next.ts). */
3234
+ async function goOn(ctx) {
3235
+ const { who, url, posted, now, response } = ctx;
3236
+ const body = readForm(posted, CONSOLE_FORMS.releaseGoOn);
3237
+ const task = matchTaskPath(url.pathname, "/release/go-on$");
3238
+ if (task === null || who.via !== "cookie")
3239
+ return missing(who, response);
3240
+ const ref = store.lookupRef(task.taskId);
3241
+ if (ref === null || !visible(ref.repo))
3242
+ return missing(who, response);
3243
+ if (!authenticateApprover(who, body.get("password") ?? "", ref.repo).ok)
3244
+ return sheetScreen(ctx, task.taskId, "Enter your Toolroll password to let this release go on.", 403);
3245
+ const allowed = allowAutoPublish(store, { taskId: task.taskId, by: who.name, now });
3246
+ if (!allowed.ok)
3247
+ return sheetScreen(ctx, task.taskId, allowed.why, 409);
3248
+ return redirect(response, releaseSheetHref(task.taskId));
3249
+ }
3250
+ return { routes: { "task.release": showSheet, "task.release.go-on": goOn, "release.locate": locate }, sheetScreen, approveFromSheet };
3398
3251
  }