toolroll 0.9.92 → 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 (341) hide show
  1. package/CHANGELOG.md +87 -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-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
@@ -21,6 +21,7 @@ import {} from "../work-index.js";
21
21
  import { HEADLINES } from "../task-status.js";
22
22
  import { chipOfHeadline } from "../design/kit.js";
23
23
  import { WORK_VIEWS } from "../workspace-ui.js";
24
+ import { tasksHref } from "./retired-routes.js";
24
25
  import { createHash } from "node:crypto";
25
26
  import { join } from "node:path";
26
27
  import { buildsViews, CHEVRON_ICON, consoleSpend, GRIP_HANDLE, isOverdue, projectChip, relativeAge, safePrUrl, screen, sentenceCase, TO_FRONT_ICON, when, whenTime } from "./chrome.js";
@@ -28,123 +29,12 @@ import { reviewHref, taskHref } from "./http.js";
28
29
  import { criterionMatrixSummary, incidentWords, PHASE_WORDS, phaseWords } from "./render-results.js";
29
30
  import { decisionAnswerCard, decisionAnswerScript, dispatchHeadline, statusLineHtml, workDiagnosticsHtml } from "./render-tasks.js";
30
31
  export const TASK_STATES = ["queued", "running", "done", "failed", "cancelled"];
31
- export const INBOX_TABS = [
32
- { id: "needs-you", label: "Needs you" }, { id: "ready", label: "Ready" }, { id: "running", label: "Running" }, { id: "all", label: "All" },
33
- ];
34
- export function parseInboxTab(raw) {
35
- return INBOX_TABS.some(one => one.id === raw) ? raw : undefined;
36
- }
37
- /** What each tab holds, as a short fingerprint: a tab is unread when its
38
- * fingerprint differs from the one this browser saw there last. */
39
- export function inboxFingerprints(data) {
40
- const print = (keys) => createHash("sha256").update([...keys].sort().join("\n")).digest("hex").slice(0, 8);
41
- const needs = [...data.decisions.map(one => `d${one.id}`), ...data.approvals.map(one => `a${one.taskId}@${one.proposedAt}`), ...data.requeueables.map(one => `q${one.taskId}:${one.strikes}`),
42
- ...data.cancelledBlockers.map(one => `c${one.blockerId}`), ...data.gaps.map(one => `g${one.key}`)];
43
- const ready = [...data.needsVerification.map(one => `v${one.taskId}`), ...(data.ready ?? []).map(one => `r${one.taskId}`)];
44
- const running = (data.running ?? []).map(one => `u${one.taskId}`);
45
- return { "needs-you": print(needs), ready: print(ready), running: print(running), all: print([...needs, ...ready, ...running]) };
46
- }
47
- export function inboxPage(chrome, data) {
48
- /** The row's project, worn openly in the roll-up — null is UNPLACED,
49
- * said as such, never a silent missing chip (finding 13). */
50
- const chip = (repo) => !data.rollup ? null : repo === null || repo === undefined
51
- ? html ` <span class="badge">Unplaced</span>`
52
- : html ` <span class="badge">${projectName(repo)}</span>`;
53
- const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
54
- data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
55
- // The roll-up inbox keeps its links-only contract — acting means opening
56
- // the project. A SELECTED project's inbox answers reversible options on
57
- // the card itself (portfolio arc §2).
58
- const decisions = data.decisions.length === 0
59
- ? null
60
- : html `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>${joinHtml(data.decisions.map(decision => data.interactive && !data.rollup
61
- ? decisionAnswerCard(decision, data.csrf, data.now, false)
62
- : html `<a class="decide-card" href="/d/${decision.id}"><p class="q">${decision.question}</p><span class="mono meta">${decision.taskId}</span>${chip(decision.repo)}${isOverdue(decision, data.now) ? html ` <span class="badge badge-overdue">Overdue</span>` : null}</a>`), "\n")}`;
63
- const approvals = data.approvals.length === 0
64
- ? null
65
- : html `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>${joinHtml(data.approvals.map(one => html `<a class="decide-card" href="${taskHref(one.taskId)}"><p class="q">${one.title}</p><span class="meta">${one.goal.length > 120 ? one.goal.slice(0, 120) + "…" : one.goal}</span><br><span class="mono meta">${one.taskId}</span>${chip(one.repo)} <span class="right meta">${one.closed == null ? html `review &amp; approve →` : html `needs attention: ${one.closed.toLowerCase()} →`}</span></a>`), "\n")}`;
66
- const requeueables = data.requeueables.length === 0
67
- ? null
68
- : html `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>${joinHtml(data.requeueables.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a> ${one.title}${chip(one.repo)}${one.incidentCount > 0 ? html ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : null}${one.strikes > 0 ? html ` <span class="meta">${one.strikes} failed attempt${one.strikes > 1 ? "s" : ""}</span>` : null}${data.rollup
69
- ? html `<span class="right meta">open its project to retry →</span></p>`
70
- : html `<span class="right">${postForm(`${taskHref(one.taskId)}/requeue`, html `<button type="submit">Retry</button>`, { attrs: { class: "inline" }, returnTo: "inbox" })}</span></p>`}`), "\n")}`;
71
- const needsVerification = data.needsVerification.length === 0
72
- ? null
73
- : html `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>${joinHtml(data.needsVerification.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a> ${one.title}${chip(one.repo)} <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}${one.repairChain == null ? null : html ` <span class="meta">— repair ${one.repairChain.outcome === "drafted" ? "drafted, awaiting approval" : one.repairChain.outcome}</span>`}</p>`), "\n")}`;
74
- const cancelled = data.cancelledBlockers.length === 0
75
- ? null
76
- : html `<h2>Choose how waiting tasks continue</h2><p class="hint">these tasks were waiting for work that was cancelled — open one and choose what happens next</p>${joinHtml(data.cancelledBlockers.map(one => html `<p class="row"><a href="${taskHref(one.exampleDependent)}">${one.exampleDependent}</a>${chip(one.repo)} <span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${one.blockerId}${data.rollup && one.repo !== one.blockerRepo ? html ` · across projects${one.repo === null || one.repo === undefined ? null : html ` — waits in ${projectName(one.repo)}`}` : null}</span></p>`), "\n")}`;
77
- const gaps = data.gaps.length === 0
78
- ? null
79
- : html `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>${joinHtml(data.gaps.map(gap => html `<p class="row"><a href="/caps">${gap.key}</a> <span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span><span class="right meta">how to fix →</span></p>`), "\n")}`;
80
- // The first-run steps, until the first Ready result: each is done or
81
- // shows the one action that does it.
82
- const steps = data.wizard;
83
- const wizard = steps === null
84
- ? null
85
- : html `<div class="card" data-first-run><p><strong>Get to your first result</strong></p>${joinHtml(steps.map(step => html `<p class="row" data-step="${step.key}"><span class="mono" aria-hidden="true">${step.done ? "✓" : "○"}</span> <strong>${step.title}</strong>${step.checking ? html ` <span class="meta">checking…</span>` : step.action === null ? html ` <span class="meta">done</span>`
86
- : step.action.kind === "link" ? html ` <a href="${step.action.href}">${step.action.label}</a>`
87
- : html ` <code>${step.action.command}</code>`}</p>`), "\n")}</div>`;
88
- const noWorker = data.worker.answering > 0
89
- ? null
90
- : html `<div class="card builder-notice" data-builder-status="${data.worker.registered === 0 ? "not-connected" : "disconnected"}">${data.worker.registered === 0
91
- ? html `<strong>No builder is connected yet.</strong> Toolroll is open, but no machine is connected to do project work. On the machine where the project lives, open that folder and run <span class="mono">toolroll up</span>. Keep Toolroll running; approved work starts automatically.`
92
- : html `<strong>Builder disconnected.</strong> ${data.worker.registered} builder${data.worker.registered === 1 ? " is" : "s are"} configured, last checked in ${data.worker.lastHeard === null ? "never" : whenTime(data.worker.lastHeard)}. Reopen Toolroll on that machine. Queued work starts automatically when a builder reconnects.`}</div>`;
93
- // Console v2: Needs you · Ready · Running · All, as real links (Back and
94
- // bookmarks work). Each section belongs to one tab; All shows every one.
95
- const tab = data.tab ?? "all";
96
- const shows = (one) => tab === "all" || tab === one;
97
- const listRows = (rows) => joinHtml(rows.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.title}</a>${chip(one.repo)} <span class="meta">${one.detail}</span></p>`), "\n");
98
- const ready = (data.ready ?? []).length === 0 ? null : html `<h2>Ready to review</h2>${listRows(data.ready ?? [])}`;
99
- const running = (data.running ?? []).length === 0 ? null : html `<h2>Running now</h2>${listRows(data.running ?? [])}`;
100
- const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
101
- const counts = { ...asks,
102
- "needs-you": asks.decide + asks.unblock,
103
- ready: data.needsVerification.length + (data.ready ?? []).length,
104
- running: (data.running ?? []).length,
105
- all: 0,
106
- };
107
- counts.all = counts["needs-you"] + counts.ready + counts.running;
108
- // Each ask is a small heading with its count over its sections (their own headings one step down).
109
- const askGroup = (ask, count, sections) => count === 0 ? null
110
- : html `<section class="inbox-ask" data-ask="${ask}"><h2>${ASK_LABEL[ask]} <span class="count">${count}</span></h2>${replaceMarkup(joinHtml(sections), /<(\/?)h2>/g, (_match, slash) => slash === "/" ? html `</h3>` : html `<h3>`)}</section>`;
111
- const tabs = data.tab === undefined ? null : html `<nav class="inbox-tabs" aria-label="Inbox views">${INBOX_TABS.map(one => html `<a href="/inbox?tab=${one.id}"${one.id === tab ? html ` aria-current="page"` : null} data-inbox-tab="${one.id}">${one.label}<span class="inbox-tab-count${one.id === "needs-you" && counts[one.id] > 0 ? " inbox-tab-count--needs" : ""}">${counts[one.id]}</span>${one.id !== tab && (data.unread ?? []).includes(one.id) ? html `<span class="inbox-unread" aria-label="new"></span>` : null}</a>`)}</nav>`;
112
- const tabEmpty = data.tab !== undefined && tab !== "all" && counts[tab] === 0
113
- ? html `<div class="card"><p class="meta">${tab === "needs-you" ? "Nothing needs you." : tab === "ready" ? "No results are waiting for review." : "Nothing is running."}</p></div>` : null;
114
- return screen("inbox", joinHtml([
115
- html `<h1>Inbox</h1>`,
116
- html `<p class="meta">Everything that waits on you — empty means the fleet is working</p>`,
117
- tabs,
118
- noWorker,
119
- wizard,
120
- empty || !shows("needs-you") ? null : html `<p><a class="new-task" style="display:inline-block" href="/next">clear the queue → one thing at a time</a></p>`,
121
- empty && data.wizard === null && shows("needs-you") && counts.all === 0 ? html `<div class="card"><p><strong>Nothing needs you.</strong></p><p class="meta">The queue is either working or waiting on its own timers. <a href="/board">Watch the board</a> or <a href="/activity">read the activity report</a>.</p></div>` : tabEmpty,
122
- // What waits on a person, grouped by what it asks: Decide, then Review, then Unblock.
123
- shows("needs-you") ? askGroup("decide", counts.decide, [approvals, decisions]) : null,
124
- shows("ready") ? askGroup("review", counts.ready, [needsVerification, ready]) : null,
125
- shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : null,
126
- shows("running") ? running : null,
127
- data.rollup
128
- ? html `<p class="meta">Requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
129
- : null,
130
- // Quick capture: the shortest path from "I want this done" to the
131
- // approve card — title and goal here, the yes on the next screen. The
132
- // one-shot form posts to the same guarded handler as the full page.
133
- data.rollup ? null : html `<h2>Capture new work</h2>`,
134
- data.rollup ? null : postForm("/tasks/add", joinHtml([
135
- html `<label>What should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
136
- html `<label>What success looks like <span class="meta">(becomes the scope you approve on the next screen)</span><textarea name="goal" rows="2"></textarea></label>`,
137
- html `<button type="submit">Queue it → approve its scope next</button>`,
138
- ], "\n"), { attrs: { class: "card" }, projectRevision: data.revision }),
139
- ], "\n"), {
140
- chrome,
141
- // The inline-answer enhancement rides only where its forms render; it
142
- // touches one card and nothing else, so quick-capture input survives.
143
- ...(data.interactive && !data.rollup && data.decisions.length > 0
144
- ? { functional: { script: decisionAnswerScript(), fetches: true } }
145
- : {}),
146
- });
147
- }
32
+ /**
33
+ * The inbox (v3): only things stalling progress without a person, one card
34
+ * per underlying stall, each with its verb inline or one step away. No
35
+ * auto-refresh — approval links lead to the step-up screen, and a page
36
+ * that might hold typed input never re-renders itself.
37
+ */
148
38
  /** Start a builder: the one command, as text to select (/start-builder, where every "Connect a builder" leads, and
149
39
  * the Agents page). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
150
40
  * that may build there is checking in (null: say nothing); `managed`: whether this person can start one themselves
@@ -201,7 +91,7 @@ export function boardWorkers(runners, scope) {
201
91
  */
202
92
  function workerStrip(runners, cards, now) {
203
93
  if (runners.length === 0)
204
- return html `<p class="meta crew-empty">No workers yet — work waits in the queue until one joins.</p>`;
94
+ return html `<p class="meta crew-empty">No builders yet — work waits in the queue until one joins.</p>`;
205
95
  const tiles = runners.map(runner => {
206
96
  const builds = cards
207
97
  .filter(card => card.lane === "building" && card.claim !== null && card.claim.runner === runner.name)
@@ -268,7 +158,7 @@ export function boardBody(data, ciRed) {
268
158
  // A lane is a section that can fold (board pass): open when it holds
269
159
  // cards, folded when empty, so a phone reads the counts first and a
270
160
  // desktop column never spends its height on "nothing here".
271
- return html `<details class="lane lane-${key}"${shown.length === 0 ? null : html ` open`}><summary><h2>${laneChip(key === "attention" && cards.length === 0 ? "queued" : key, title)} <span class="lane-count">${cards.length}${data.saturated ? "+" : ""}</span></h2></summary><p class="hint">${hint}</p>${shown.length === 0 ? html `<p class="meta lane-empty">nothing here</p>` : shown.map((card, index) => renderOne(card, index))}${more > 0 ? html `<a class="lane-more" href="/work">+${more} more in Tasks</a>` : null}</details>`;
161
+ return html `<details class="lane lane-${key}"${shown.length === 0 ? null : html ` open`}><summary><h2>${laneChip(key === "attention" && cards.length === 0 ? "queued" : key, title)} <span class="lane-count">${cards.length}${data.saturated ? "+" : ""}</span></h2></summary><p class="hint">${hint}</p>${shown.length === 0 ? html `<p class="meta lane-empty">nothing here</p>` : shown.map((card, index) => renderOne(card, index))}${more > 0 ? html `<a class="lane-more" href="/tasks">+${more} more in Tasks</a>` : null}</details>`;
272
162
  };
273
163
  // The card's facts: mono key–value pairs under the title (board pass) —
274
164
  // task, worker, runtime — the same grammar on every lane, so the eye
@@ -300,7 +190,7 @@ export function boardBody(data, ciRed) {
300
190
  return heads;
301
191
  })();
302
192
  const queuedCard = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="t">${card.title}</span><span class="why">${card.reason}</span><span class="id">${card.taskId}</span>${facts([
303
- card.assignedRunner === null ? ["worker", "any free worker"] : ["worker", card.assignedRunner, "mono"],
193
+ card.assignedRunner === null ? ["builder", "any free builder"] : ["builder", card.assignedRunner, "mono"],
304
194
  ])}${chips([
305
195
  queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
306
196
  ? html `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${card.assignedRunner}`}</span>`
@@ -357,8 +247,8 @@ export function boardBody(data, ciRed) {
357
247
  : html `<p class="meta board-scope">${data.all
358
248
  ? html `${data.project === null
359
249
  ? html `<strong>All projects</strong>`
360
- : html `<a href="/board">${projectName(data.project)}</a> · <strong>all projects</strong>`} — every project this server serves, each card wearing its project`
361
- : html `<strong>${projectName(data.project)}</strong> · <a href="/board?scope=all">all projects</a>`}</p>`;
250
+ : html `<a href="/tasks?view=board">${projectName(data.project)}</a> · <strong>all projects</strong>`} — every project this server serves, each card wearing its project`
251
+ : html `<strong>${projectName(data.project)}</strong> · <a href="/tasks?view=board&scope=all">all projects</a>`}</p>`;
362
252
  const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
363
253
  const deltaLine = data.delta === null
364
254
  ? null
@@ -366,21 +256,21 @@ export function boardBody(data, ciRed) {
366
256
  data.delta.built > 0 ? html `<span class="good">${data.delta.built} built</span>` : null,
367
257
  data.delta.failed > 0 ? html `<span class="bad">${data.delta.failed} failed</span>` : null,
368
258
  data.delta.questions > 0
369
- ? html `${data.delta.questions} question${data.delta.questions > 1 ? "s" : ""} — <a href="/next">answer →</a>`
259
+ ? html `${data.delta.questions} question${data.delta.questions > 1 ? "s" : ""} — <a href="/">answer →</a>`
370
260
  : null,
371
261
  ].filter((one) => one !== null), html ` · `)}</p>`;
372
262
  return joinHtml([
373
263
  html `<h1>Board</h1>`,
374
- data.all ? null : html `<p class="meta board-view"><strong>State</strong> · <a href="/board?view=order">order →</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
264
+ data.all ? null : html `<p class="meta board-view"><strong>State</strong> · <a href="/tasks?view=order">order →</a> <span class="meta">drag to reorder, or to keep a task for one builder</span></p>`,
375
265
  deltaLine,
376
266
  toggle,
377
267
  workerStrip(data.workers, data.cards, data.now),
378
268
  html `<div class="board" data-now="${String(data.now.getTime())}">`,
379
269
  lane("attention", "Needs you", "These wait for a person.", plain),
380
- lane("queued", "Queued", "Starts when a worker is free.", queuedCard),
270
+ lane("queued", "Queued", "Starts when a builder is free.", queuedCard),
381
271
  lane("waiting", "Waiting", "Paused until a time, another task, or a requirement is ready.", plain),
382
272
  lane("building", "Building", "One agent per card, in its own workspace.", building),
383
- html `<details class="lane lane-done"${data.done.length === 0 ? null : html ` open`}><summary><h2>${laneChip("done", html `<a href="/done">Done recently</a>`)}</h2></summary><p class="hint">The most recent. The full list is under Done.</p>${doneCards}</details>`,
273
+ html `<details class="lane lane-done"${data.done.length === 0 ? null : html ` open`}><summary><h2>${laneChip("done", html `<a href="/tasks?view=done">Done recently</a>`)}</h2></summary><p class="hint">The most recent. The full list is under Done.</p>${doneCards}</details>`,
384
274
  html `</div>`,
385
275
  ], "\n");
386
276
  }
@@ -441,7 +331,7 @@ export function homePage(chrome, data) {
441
331
  html `<div class="card">`,
442
332
  html `<p><strong>Nothing is queued yet — here is the whole loop:</strong></p>`,
443
333
  html `<p>1. <a href="/tasks/new">Add a task</a> — plain words for work you want done${data.repo === null ? null : html ` in <span class="mono">${data.repo}</span>`}.</p>`,
444
- html `<p>2. Open it and write its scope — the goal, and what it must not become. Approve exactly that.</p>`,
334
+ html `<p>2. Open it and write its plan — the goal, and what it must not become. Approve exactly that.</p>`,
445
335
  html `<p>3. Keep Toolroll running on the builder machine. Approved tasks build unattended, each on its own branch.</p>`,
446
336
  html `<p class="meta">When an agent is unsure it stops and asks — those questions land here, under “waiting on you”.</p>`,
447
337
  html `</div>`,
@@ -493,28 +383,17 @@ export function workPage(chrome, data) {
493
383
  const layout = data.board != null ? 'board' : 'list';
494
384
  // The project and the stored-state filter ride every link; a change of filter starts on page one.
495
385
  const query = (over = {}) => {
496
- const q = new URLSearchParams();
497
386
  const project = over.project === undefined ? (data.projectFilter ?? (data.all === true ? 'all' : null)) : over.project;
498
- if (project === 'all')
499
- q.set('all', '1');
500
- else if (project !== null)
501
- q.set('project', project);
502
- const view = over.view ?? data.view;
503
- if (view !== 'all')
504
- q.set('view', view);
387
+ const board = (over.layout ?? layout) === 'board';
505
388
  const state = over.state === undefined ? data.state ?? null : over.state;
506
- if (state !== null)
507
- q.set('state', state);
508
- if (over.cursor !== undefined)
509
- q.set('cursor', over.cursor);
510
- if ((over.layout ?? layout) === 'board')
511
- q.set('layout', 'board');
512
- return `/work${q.size ? `?${q}` : ''}`;
389
+ // Tasks' own address (docs/plans/plain-toolroll.md P1.4): Board is a view; every project is scope=all.
390
+ return tasksHref({ scope: project === 'all' ? 'all' : null, project: project === 'all' ? null : project, view: board ? 'board' : over.view ?? data.view,
391
+ state: board ? null : state, cursor: over.cursor ?? null });
513
392
  };
514
393
  const href = (view, cursor, state = data.state ?? null) => query({ view, ...(cursor === undefined ? {} : { cursor }), state });
515
394
  // Put away (or Restore, in Archived): one quiet act per row, never on a task with work going on.
516
395
  const tidyAct = (row) => data.canPutAway !== true ? null : row.archived !== undefined ? { action: `${taskHref(row.rootId)}/restore`, label: 'Restore' }
517
- : row.liveRunId !== null || row.unfinishedRunId !== null || row.status.views.includes('running') ? null : { action: `${taskHref(row.rootId)}/archive`, label: 'Put away' };
396
+ : row.liveRunId !== null || row.unfinishedRunId !== null || row.status.views.includes('running') ? null : { action: `${taskHref(row.rootId)}/archive`, label: 'Archive' };
518
397
  const returnTo = href(data.view, undefined);
519
398
  const current = WORK_VIEWS.find(one => one.key === data.view);
520
399
  const filtered = (data.state ?? null) !== null;
@@ -559,8 +438,8 @@ export function workPage(chrome, data) {
559
438
  ['running', 'Running', data.board.running, 'running', 'Nothing running.'],
560
439
  ['finished', 'Finished', data.board.finished, 'completed', 'Nothing has finished yet.'],
561
440
  ].map(([key, label, page, view, empty]) => ({ key, label, count: page.totals[view], href: query({ view, layout: 'list' }), empty, rows: page.items.map(rowOf) }));
562
- const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Put away old tasks', href: '/work/put-away' } : null;
563
- const toolLinks = [['/board?view=order', 'Order the queue'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']];
441
+ const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Archive old tasks', href: '/tasks/archive' } : null;
442
+ const toolLinks = [['/tasks?view=order', 'Order the queue'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Action ledger']];
564
443
  const newTaskHref = lens === null ? '/tasks/new' : `/tasks/new?project=${encodeURIComponent(lens)}`;
565
444
  const emptyNow = data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: 'Show every state', href: href(data.view, undefined, null) } } : {
566
445
  text: data.previous ? 'There are no more tasks on this page.' : current.empty,
@@ -617,162 +496,10 @@ export function workPage(chrome, data) {
617
496
  ? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
618
497
  : html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
619
498
  const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
620
- const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/work?layout=board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
499
+ const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/', 'Home'], ['/tasks?view=board', 'Board'], ['/tasks?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Action ledger']]
621
500
  .map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
622
501
  return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${legacyTabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
623
502
  }
624
- /**
625
- * The control-room rail (attended A1): every state that matters while a
626
- * person supervises, ordered by intervention cost. The project chip is
627
- * deliberately repeated on every row — a title is not a workspace, and
628
- * switching context must never depend on remembering which repo is open.
629
- */
630
- export function workbenchRail(data) {
631
- const workspace = (repo) => html `<span class="badge">${repo === null ? "Unplaced" : projectName(repo)}</span>`;
632
- const row = (card, reason) => html `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"${card.taskId === data.selected ? html ` aria-current="true"` : null}><span class="wb-title">${card.title}</span><span class="wb-meta"><span class="mono meta">${card.taskId}</span>${workspace(card.repo)}</span><span class="wb-reason">${reason}</span></a>`;
633
- const group = (title, cards, empty, render) => html `<section class="wb-group"><h2>${title} <span class="lane-count">${cards.length}</span></h2>${cards.length === 0 ? html `<p class="meta">${empty}</p>` : joinHtml(cards.slice(0, 100).map(render), "\n")}</section>`;
634
- const parts = [];
635
- parts.push(html `<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>All projects</h2></div><a href="/projects">manage →</a></div>`);
636
- parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, card.reason)));
637
- parts.push(group("in progress", data.building, "No agent is working right now.", card => {
638
- const claim = card.claim;
639
- const phase = claim?.phase == null ? "working" : phaseWords(claim.phase);
640
- return row(card, html `${phase}${claim?.provider ? html ` · ${claim.provider}` : null}${claim?.claimedAt ? html ` · <time data-elapsed-since="${claim.claimedAt}"></time>` : null}`);
641
- }));
642
- parts.push(group(html `blocked & waiting`, data.waiting, "Nothing is blocked or paused.", card => row(card, card.reason)));
643
- parts.push(group("up next", data.queued, "The ready queue is empty.", card => row(card, html `${card.reason}${card.assignedRunner === null ? null : html ` · reserved for ${card.assignedRunner}`}`)));
644
- if (data.done.length > 0) {
645
- parts.push(html `<section class="wb-group"><h2>Just finished <span class="lane-count">${data.done.length}</span></h2>`);
646
- parts.push(joinHtml(data.done.map(one => html `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}"><span class="wb-title">${one.title}</span><span class="wb-meta"><span class="mono meta">${one.taskId}</span>${workspace(one.repo)} <span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${sentenceCase(one.outcome ?? "?")}</span></span></a>`), "\n"));
647
- parts.push(html `</section>`);
648
- }
649
- if (data.saturated)
650
- parts.push(html `<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
651
- return joinHtml(parts, "\n");
652
- }
653
- /**
654
- * The portfolio overview (arc slice 1a): what waits on you, what the last
655
- * 24 hours of run starts amounted to, what is running, and the terminal-run
656
- * ledger — all of it across every admitted project, a project chip on every
657
- * row. The caller has already applied admission and per-row visibility.
658
- */
659
- export function portfolioOverview(data) {
660
- const pulses = new Map();
661
- const pulseFor = (repo) => {
662
- const key = repo ?? "";
663
- const existing = pulses.get(key);
664
- if (existing !== undefined)
665
- return existing;
666
- const made = { repo, attention: 0, building: 0, waiting: 0, queued: 0, done: 0 };
667
- pulses.set(key, made);
668
- return made;
669
- };
670
- for (const card of [...data.attention, ...data.building, ...data.waiting, ...data.queued]) {
671
- pulseFor(card.repo)[card.lane] += 1;
672
- }
673
- for (const one of data.done)
674
- pulseFor(one.repo).done += 1;
675
- const pulseRows = [...pulses.values()].sort((a, b) => b.attention - a.attention || b.building - a.building || b.waiting - a.waiting ||
676
- (a.repo === null ? 1 : b.repo === null ? -1 : projectName(a.repo).localeCompare(projectName(b.repo))));
677
- const workspace = (repo) => repo === null ? "Unplaced work" : projectName(repo);
678
- // A workspace card (board pass): the repo's name and one status word,
679
- // its four counts, a bar of the same counts in proportion, and — for a
680
- // real repo, with a session token — the one tap to its own board. The
681
- // status word is the loudest true thing: needs you beats building beats
682
- // waiting beats queued; a repo with nothing in flight is idle.
683
- const statusOf = (one) => one.attention > 0
684
- ? { word: "needs you", cls: "badge-open" }
685
- : one.building > 0
686
- ? { word: "building", cls: "badge-running" }
687
- : one.waiting > 0
688
- ? { word: "waiting", cls: "" }
689
- : one.queued > 0
690
- ? { word: "queued", cls: "" }
691
- : { word: "idle", cls: "" };
692
- const workspaceRows = joinHtml(pulseRows.map(one => {
693
- const status = statusOf(one);
694
- const total = one.attention + one.building + one.waiting + one.queued;
695
- const seg = (cls, count) => count === 0 ? null : html `<span class="seg ${cls}" style="flex-grow:${count}"></span>`;
696
- const boardForm = one.repo === null || data.csrf === ""
697
- ? null
698
- : postForm("/projects/open", html `<button type="submit">Board →</button>`, { attrs: { class: "inline" }, returnTo: "/board", hidden: { path: one.repo } });
699
- const setUpForm = one.repo === null || data.csrf === ""
700
- ? null
701
- : postForm("/projects/open", html `<button>Set up →</button>`, { attrs: { class: "inline" }, returnTo: "/control", hidden: { path: one.repo } });
702
- return html `<div class="workspace-card${one.attention > 0 ? " hot" : ""}"><div class="workspace-head"><span class="workspace-name">${workspace(one.repo)}</span><span class="badge ${status.cls}">${sentenceCase(status.word)}</span>${boardForm}${setUpForm}</div><div class="workspace-stats"><span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span><span class="pulse-stat"><b>${one.building}</b> live</span><span class="pulse-stat"><b>${one.waiting}</b> waiting</span><span class="pulse-stat"><b>${one.queued}</b> next</span></div><div class="workspace-bar${total === 0 ? " empty" : ""}" aria-hidden="true">${seg("attention", one.attention)}${seg("building", one.building)}${seg("waiting", one.waiting)}${seg("queued", one.queued)}</div></div>`;
703
- }), "\n");
704
- // ---- waits on you: everything a person must resolve, across projects ----
705
- const waitCount = data.decisions.length + data.approvals.length + data.requeueables.length +
706
- data.cancelledBlockers.length + data.gaps.length;
707
- const decisionCards = joinHtml(data.decisions.map(one => decisionAnswerCard(one, data.csrf, data.now, true)), "\n");
708
- const approvalCards = joinHtml(data.approvals.map(one => html `<a class="decide-card" href="${taskHref(one.taskId)}"><p class="q">${one.title}</p><span class="meta">${one.goal.length > 120 ? one.goal.slice(0, 120) + "…" : one.goal}</span><br><span class="mono meta">${one.taskId}</span>${projectChip(one.repo)} <span class="right meta">review and sign →</span></a>`), "\n");
709
- const requeueRows = joinHtml(data.requeueables.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a> ${one.title}${projectChip(one.repo)}${one.incidentCount > 0 ? html ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : null}${one.strikes > 0 ? html ` <span class="meta">${one.strikes} failed attempt${one.strikes > 1 ? "s" : ""}</span>` : null}<span class="right meta">open the task to retry →</span></p>`), "\n");
710
- const cancelledRows = joinHtml(data.cancelledBlockers.map(one => html `<p class="row"><a href="${taskHref(one.exampleDependent)}">${one.exampleDependent}</a>${projectChip(one.repo)} <span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${one.blockerId}</span><span class="right meta">choose what happens →</span></p>`), "\n");
711
- const gapRows = joinHtml(data.gaps.map(gap => html `<p class="row"><a href="/caps">${gap.key}</a>${data.gapsProject === null ? null : projectChip(data.gapsProject)} <span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span><span class="right meta">how to fix →</span></p>`), "\n");
712
- // ---- the last 24 hours: runs STARTED in the window, outcomes exhaustive ----
713
- const groups = [
714
- ["built", data.runs24.filter(one => one.outcome === "built").length],
715
- ["no change", data.runs24.filter(one => one.outcome === "no-change").length],
716
- ["failed", data.runs24.filter(one => one.outcome === "failed").length],
717
- ["refused", data.runs24.filter(one => one.outcome === "refused").length],
718
- ["parked", data.runs24.filter(one => one.outcome === "parked").length],
719
- ["interrupted", data.runs24.filter(one => one.outcome === "interrupted").length],
720
- ["unfinished", data.runs24.filter(one => one.outcome === null).length],
721
- ];
722
- const outcomeWords = groups.filter(([, count]) => count > 0).map(([word, count]) => `${count} ${word}`).join(" · ");
723
- const summary = tally(data.runs24);
724
- // ---- running: current live claims, project chips on ----
725
- const liveRows = joinHtml(data.live.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a>${projectChip(one.repo)} <span class="badge badge-running">Running</span> <span class="meta"><span class="mono">${one.runner}</span>${one.model === null ? null : html ` · ${modelName(null, one.model)}`} · <time data-elapsed-since="${one.claimedAt}"></time></span></p>`), "\n");
726
- // ---- the ledger: terminal runs started in the window, one chip each ----
727
- const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
728
- const prLink = (one) => {
729
- if (one.prNumber === null)
730
- return null;
731
- // The URL-sink rule (audit IV-11): only a verified github pull
732
- // URL earns an anchor; a corrupted row renders as text.
733
- const safe = safePrUrl(one.prUrl);
734
- return safe === null ? html ` · PR #${one.prNumber}` : html ` · <a href="${safe}">PR #${one.prNumber}</a>`;
735
- };
736
- const ledgerRows = joinHtml(data.ledger.map(one => html `<p class="row"><a href="/r/${one.runId}">${one.title}</a>${projectChip(one.repo)} <span class="badge ${chipClass(one.outcome)}">${sentenceCase(one.outcome)}</span> ${one.role === "scout" ? html `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : null}<span class="meta">${one.provider === null ? (one.model === null ? "" : modelName(null, one.model)) : agentModelName(one.provider, one.model)}${one.ranMinutes === null ? null : html ` · ${one.ranMinutes}m`} · ${runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null })}${prLink(one)}</span></p>`), "\n");
737
- return joinHtml([
738
- html `<div class="control-room-head"><div><h1>Portfolio</h1><p class="meta">Every project and live build in one place</p></div><div class="actions"><a class="badge" href="/tasks/new">+ New task</a><a class="badge" href="/board?scope=all">Full board →</a></div></div>`,
739
- data.saturated ? html `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : null,
740
- html `<h2>Waits on you</h2>`,
741
- waitCount === 0
742
- ? html `<div class="answered"><strong>Nothing needs you.</strong> <span class="meta">You can leave this open; live state updates in the rail.</span></div>`
743
- : joinHtml([
744
- data.decisions.length === 0 ? null : decisionCards,
745
- data.approvals.length === 0 ? null : approvalCards,
746
- data.requeueables.length === 0 ? null : requeueRows,
747
- data.cancelledBlockers.length === 0 ? null : cancelledRows,
748
- data.gaps.length === 0 ? null : gapRows,
749
- html `<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
750
- ].filter((part) => part !== null), "\n"),
751
- data.gapsProject === null
752
- ? html `<p class="meta">Requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
753
- : null,
754
- html `<h2>Project pulse</h2>`,
755
- html `<p class="hint">one row per repository</p>`,
756
- pulseRows.length === 0
757
- ? html `<div class="card"><p><strong>No active work yet.</strong></p><p class="meta">Queue a task and its progress will show here across every workspace.</p></div>`
758
- : html `<div class="workspace-pulse">${workspaceRows}</div>`,
759
- html `<h2>The last 24 hours</h2>`,
760
- html `<p class="hint">runs started in the last 24 hours</p>`,
761
- data.runs24.length === 0
762
- ? html `<p class="meta">No runs started in the window</p>`
763
- : html `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p><p class="row"><span class="meta">outcomes</span> <span class="mono">${outcomeWords}</span></p><p class="row"><span class="meta">spend</span> <span class="mono">${spendLine(summary)}</span></p>${
764
- // Tokens stand on their own: invocations that reported usage —
765
- // independent of whether cost was measured (spec §2; spendLine's
766
- // mixed branch omits them).
767
- summary.tokens > 0
768
- ? html `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
769
- : null}`,
770
- html `<h2>Running</h2>`,
771
- data.live.length === 0 ? html `<p class="meta">No agent is working right now</p>` : liveRows,
772
- html `<h2>Terminal runs started in the last 24 hours</h2>`,
773
- data.ledger.length === 0 ? html `<p class="meta">None yet</p>` : ledgerRows,
774
- ], "\n");
775
- }
776
503
  /**
777
504
  * owner/name from a git remote URL — FULLY ANCHORED https/ssh github.com
778
505
  * forms only (ghlist review, finding 2): a foreign host carrying
@@ -797,7 +524,7 @@ export function queueCard(one, revision, queueRevision, workers, column) {
797
524
  // and the reservation owner. Money is not in this query and is not
798
525
  // invented here — it stays on the task page, labeled.
799
526
  const state = one.taken
800
- ? html `<span class="badge">Being taken — keeps its claim</span>`
527
+ ? html `<span class="badge">Starting — a builder has it</span>`
801
528
  : column === "anyone"
802
529
  ? html `<span class="badge">Queued</span>`
803
530
  : html `<span class="badge">Reserved for ${column}</span>`;
@@ -820,12 +547,12 @@ export function queueBody(tasks, workers, revision, queueRevision) {
820
547
  const column = (title, key, head, rows, empty) => html `<section class="lane queue-column" data-column="${key}"><h2>${title}</h2>${head}<p class="meta">${projectChips(rows)}</p>${rows.length === 0
821
548
  ? html `<p class="meta lane-empty">${empty}</p>`
822
549
  : joinHtml(rows.map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
823
- ? replaceMarkup(replaceMarkup(queueCard(one, revision, queueRevision, workers, key), /<\/p>\n/, () => html `</p>`), /<p class="row meta">/, () => html `<p class="meta">Every worker has reserved work — this waits until a column empties</p><p class="row meta">`)
550
+ ? replaceMarkup(replaceMarkup(queueCard(one, revision, queueRevision, workers, key), /<\/p>\n/, () => html `</p>`), /<p class="row meta">/, () => html `<p class="meta">Every builder has work kept for it — this waits until a column empties</p><p class="row meta">`)
824
551
  : queueCard(one, revision, queueRevision, workers, key)), "\n")}</section>`;
825
552
  const noteForm = (worker) => worker.retired
826
- ? html `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
553
+ ? html `<p class="meta">This builder is retired — drag these elsewhere, or register the name again</p>`
827
554
  : html `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>${postForm("/queue/note", html `<input type="text" name="note" value="${worker.note ?? ""}" data-initial="${worker.note ?? ""}" placeholder="what this worker is working through" aria-label="column note" maxlength="200"><button type="submit">Save</button>`, { attrs: { class: "row" }, projectRevision: revision, hidden: { runner: worker.name } })}<p class="meta">Takes from the shared queue when this column is empty</p>`;
828
- return html `<div class="lanes" data-queue-revision="${queueRevision}">${column("shared queue", "anyone", html `<p class="meta">Workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running")}${joinHtml(workers.map(worker => column(worker.name + (worker.retired ? " (retired)" : ""), worker.name, noteForm(worker), columnOf(worker.name), "nothing queued — this worker will take from the shared queue")), "\n")}</div>`;
555
+ return html `<div class="lanes" data-queue-revision="${queueRevision}">${column("shared queue", "anyone", html `<p class="meta">Workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running")}${joinHtml(workers.map(worker => column(worker.name + (worker.retired ? " (retired)" : ""), worker.name, noteForm(worker), columnOf(worker.name), "nothing queued — this builder takes from the shared queue")), "\n")}</div>`;
829
556
  }
830
557
  /** The /queue page's one nonce'd script: delegated pointer-event drag (it
831
558
  * survives every fragment swap — finding 17) plus a poller that re-checks
@@ -848,7 +575,7 @@ export function queueScript() {
848
575
  `function tell(){if(!stamp)return;if(paused()){stamp.textContent="paused while you edit";return;}` +
849
576
  `stamp.textContent="updated "+Math.round((Date.now()-last)/1000)+"s ago";}setInterval(tell,1000);` +
850
577
  `function cycle(){if(document.hidden||busy||paused()){setTimeout(cycle,wait);return;}busy=true;` +
851
- `fetch("/queue?fragment=1",{redirect:"manual",cache:"no-store"})` +
578
+ `fetch("/tasks?view=order&fragment=1",{redirect:"manual",cache:"no-store"})` +
852
579
  `.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return null;}` +
853
580
  `return r.ok?r.text():null;})` +
854
581
  `.then(function(t){if(t&&!paused()){region.innerHTML=t;last=Date.now();}})` +
@@ -882,13 +609,13 @@ export function queueScript() {
882
609
  `var old=card.querySelector(".queue-problem");if(old)old.remove();` +
883
610
  `var row=document.createElement("p");row.className="problem queue-problem";row.setAttribute("role","alert");row.textContent=text;card.appendChild(row);return true;}` +
884
611
  `fetch("/queue/move",{method:"POST",headers:(window.soKeyHeaders||function(h){return h})({"Content-Type":"application/x-www-form-urlencoded"}),body:post.toString(),redirect:"manual"})` +
885
- `.then(function(r){if(r.ok)return fetch("/queue?fragment=1",{redirect:"manual",cache:"no-store"}).then(function(f){` +
612
+ `.then(function(r){if(r.ok)return fetch("/tasks?view=order&fragment=1",{redirect:"manual",cache:"no-store"}).then(function(f){` +
886
613
  `if(f.type==="opaqueredirect"||f.status===401||f.status===403){location.href="/login";return false;}return f.ok?f.text():null;});` +
887
614
  `if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return false;}` +
888
615
  `var kind=(r.headers&&r.headers.get?r.headers.get("content-type"):"")||"";` +
889
616
  `if(r.status===409&&kind.indexOf("text/plain")===0){return r.text().then(function(text){return problem(drag.task,text)?false:null;});}` +
890
617
  `return null;})` +
891
- `.then(function(t){if(t===false)return;if(t&&!paused()){region.innerHTML=t;last=Date.now();}else if(t===null){location.href="/board?view=order";}})` +
618
+ `.then(function(t){if(t===false)return;if(t&&!paused()){region.innerHTML=t;last=Date.now();}else if(t===null){location.href="/tasks?view=order";}})` +
892
619
  `.catch(function(){})` +
893
620
  `.then(function(){tell();});});` +
894
621
  `})();`);
@@ -898,16 +625,14 @@ export function queueScript() {
898
625
  export function putAwayPage(chrome, data) {
899
626
  const groups = tidyGroups(data.items, data.days);
900
627
  const count = data.items.length;
901
- const daysArg = data.days === TIDY_DEFAULT_DAYS ? "" : ` --days ${data.days}`;
902
- const crumbs = html `<nav class="kit-crumbs" aria-label="Breadcrumb"><ol><li><a href="/work">Tasks</a></li><li><span aria-current="page">Put away old tasks</span></li></ol></nav>`;
903
- const daysForm = html `<form method="get" action="/work/put-away" class="put-away-days"><label>Also finished tasks untouched for <input type="number" name="days" min="1" max="365" value="${String(data.days)}" inputmode="numeric"> days</label><button type="submit" class="secondary">Update list</button></form>`;
628
+ const crumbs = html `<nav class="kit-crumbs" aria-label="Breadcrumb"><ol><li><a href="/tasks">Tasks</a></li><li><span aria-current="page">Archive old tasks</span></li></ol></nav>`;
629
+ const daysForm = html `<form method="get" action="/tasks/archive" class="put-away-days"><label>Also finished tasks untouched for <input type="number" name="days" min="1" max="365" value="${String(data.days)}" inputmode="numeric"> days</label><button type="submit" class="secondary">Update list</button></form>`;
904
630
  const list = joinHtml(groups.map(group => html `<fieldset class="put-away-group" data-tidy-group="${group.rule}"><legend>${group.label}<span class="count">${String(group.items.length)}</span></legend><ul>${group.items.map(item => html `<li><label class="put-away-item" data-tidy-task="${item.taskId}"><input type="checkbox" name="task" value="${item.taskId}" checked><span class="put-away-title">${item.title}</span><span class="put-away-why">${item.reason}</span><span class="put-away-id">${item.taskId}${item.repo === null ? "" : ` · ${projectName(item.repo)}`}${item.members.length > 1 ? ` · ${item.members.length} versions` : ""}</span></label></li>`)}</ul></fieldset>`));
905
- const cli = html `<p class="put-away-cli" data-tidy-cli>Or run: <code>toolroll task tidy --apply --digest ${data.digest}${daysArg}</code></p>`;
906
631
  const body = count === 0
907
- ? html `<p class="kit-state">Nothing to put away right now.</p><p><a href="/work?view=archived">See archived tasks →</a></p>`
632
+ ? html `<p class="kit-state">Nothing to archive right now.</p><p><a href="/tasks?view=archived">See archived tasks →</a></p>`
908
633
  : stepUpForm({ proof: "tasks-put-away", action: "/work/put-away", id: "put-away", hidden: { digest: data.digest, days: data.days },
909
- body: html `${list}${cli}`, save: { label: "Put away selected", idle: `${count} ${count === 1 ? "task" : "tasks"} selected` }, confirm: `Put away the selected tasks. You can restore any of them from Archived.` });
910
- return screen("Put away old tasks", html `<div class="put-away">${crumbs}<h1>Put away old tasks</h1>\
911
- <p class="kit-state">${count === 0 ? "Old releases, routine runs and fixed CI notices are put away automatically." : `${count} ${count === 1 ? "task" : "tasks"} can be put away. They leave every list and keep their history.`}</p>\
634
+ body: html `${list}`, save: { label: "Archive selected", idle: `${count} ${count === 1 ? "task" : "tasks"} selected` }, confirm: `Archive the selected tasks. You can restore any of them from Archived.` });
635
+ return screen("Archive old tasks", html `<div class="put-away">${crumbs}<h1>Archive old tasks</h1>\
636
+ <p class="kit-state">${count === 0 ? "Old releases, routine runs and fixed CI notices are archived automatically." : `${count} ${count === 1 ? "task" : "tasks"} can be archived. They leave every list and keep their history.`}</p>\
912
637
  ${data.problem ? html `<p class="kit-problem" role="alert">${data.problem}</p>` : ""}${data.releasesSkipped === null ? "" : html `<p class="kit-hint">Release tasks were left out: ${data.releasesSkipped}.</p>`}${daysForm}${body}</div>`, { chrome });
913
638
  }
@@ -6,7 +6,7 @@ import { type OnboardCardState, type ProjectPeek } from "./render-pages.js";
6
6
  /** A path as a person reads it: the home folder as ~. */
7
7
  export declare function shortPathOf(path: string): string;
8
8
  /** A project's page address. Names are folder names, so two projects can share one: then the path picks. */
9
- export declare function projectHref(name: string, path: string, ambiguous: boolean): string;
9
+ export declare function projectHref(name: string, path: string, ambiguous: boolean, page?: string): string;
10
10
  /** What a project's counts say on one line ("2 need you · 1 running"); null when it wasn't scanned. */
11
11
  export declare function peekWords(peek: ProjectPeek | null): string | null;
12
12
  /** Where a message belongs: next to the GitHub link, in the clone review, the page's own refusal, or a neutral line
@@ -64,6 +64,7 @@ export declare function projectPage(chrome: Chrome, data: {
64
64
  on: boolean;
65
65
  } | null;
66
66
  knowledgeHref: string;
67
+ flakyHref: string;
67
68
  settingsHref: string | null;
68
69
  activity: {
69
70
  report: ProjectOutcomes | null;
@@ -19,8 +19,8 @@ export function shortPathOf(path) {
19
19
  return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
20
20
  }
21
21
  /** A project's page address. Names are folder names, so two projects can share one: then the path picks. */
22
- export function projectHref(name, path, ambiguous) {
23
- return `/projects/${encodeURIComponent(name)}${ambiguous ? `?path=${encodeURIComponent(path)}` : ""}`;
22
+ export function projectHref(name, path, ambiguous, page = "") {
23
+ return `/projects/${encodeURIComponent(name)}${page === "" ? "" : `/${page}`}${ambiguous ? `?path=${encodeURIComponent(path)}` : ""}`;
24
24
  }
25
25
  /** What a project's counts say on one line ("2 need you · 1 running"); null when it wasn't scanned. */
26
26
  export function peekWords(peek) {
@@ -161,22 +161,24 @@ export function projectPage(chrome, data) {
161
161
  const counts = peek === null
162
162
  ? note("Not scanned yet.")
163
163
  : html `<p>${joinHtml([
164
- countWords(peek.waiting, peek.waiting === 1 ? "needs you" : "need you", "/work"),
165
- countWords(peek.running, "running", "/work"),
166
- countWords(peek.queued, "queued", "/board?view=order"),
167
- countWords(peek.doneRecently, "built today", "/done"),
164
+ countWords(peek.waiting, peek.waiting === 1 ? "needs you" : "need you", "/tasks"),
165
+ countWords(peek.running, "running", "/tasks"),
166
+ countWords(peek.queued, "queued", "/tasks?view=order"),
167
+ countWords(peek.doneRecently, "built today", "/tasks?view=done"),
168
168
  ], " · ")}</p>`;
169
169
  const tasks = data.tasks.length === 0
170
170
  ? note(`Nothing needs you or is running in ${data.name}.`)
171
171
  : html `${data.tasks.map(task => row({
172
172
  href: task.href, chip: task.needs ? status("needs") : status("running"), title: task.title, desc: task.reason, wait: task.needs,
173
173
  meta: task.since === null ? "" : html `<span class="k-num">${relativeAge(task.since, data.now)}</span>`,
174
- }))}${data.moreTasks > 0 ? note(data.open ? html `<a class="k-link" href="/work">${data.moreTasks} more in Tasks</a>` : `${data.moreTasks} more`) : ""}`;
174
+ }))}${data.moreTasks > 0 ? note(data.open ? html `<a class="k-link" href="/tasks">${data.moreTasks} more in Tasks</a>` : `${data.moreTasks} more`) : ""}`;
175
175
  const setup = facts([
176
176
  ["Folder", mono(data.path)],
177
177
  ...(data.checks === null ? [] : [["Checks", html `<a class="k-link" href="${data.checks.href}">${data.checks.level}</a>`]]),
178
178
  ...(data.pullRequests === null ? [] : [["Pull requests", html `<a class="k-link" href="${data.pullRequests.href}">${data.pullRequests.on ? "On" : "Off"}</a>`]]),
179
179
  ["Knowledge", html `<a class="k-link" href="${data.knowledgeHref}">Open knowledge</a>`],
180
+ ["Code map", html `<a class="k-link" href="${`/projects/${encodeURIComponent(data.name)}/map${data.selfHref.includes("?") ? data.selfHref.slice(data.selfHref.indexOf("?")) : ""}`}">Open code map</a>`],
181
+ ["Flaky tests", html `<a class="k-link" href="${data.flakyHref}">Open flaky tests</a>`],
180
182
  ]);
181
183
  const body = joinHtml([
182
184
  phoneHeader(data.name, { back: "/projects" }),