toolroll 0.9.85 → 0.9.87

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 (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { releaseProgressFor, type ShipChecks } from "../store.js";
1
2
  import { type Html } from "../html.js";
2
3
  import { type AssignmentSnapshot } from '../assignment.js';
3
4
  import { type FailureExplanation } from '../needs-you.js';
@@ -12,6 +13,7 @@ import { type CompletionReceiptView, type ResultDetail, type TaskPullRequest } f
12
13
  import { taskBody, type MilestoneProgressView, type PlanContractView, type PlanRevisionLedgerView, type RevisionDocView, type RevisionView, type RouteView } from "./render-tasks.js";
13
14
  import { type Who } from "./session.js";
14
15
  import type { TaskPageExtras } from "./runtime.js";
16
+ import type { BrowserTaskView } from "../browser-workspace.js";
15
17
  import type { PlanRevision, TaskRef } from "../store.js";
16
18
  import { type Run, type Store, type Task, type TaskState } from "../store.js";
17
19
  import { type ReviewFacts } from "../workspace-ui.js";
@@ -88,3 +90,9 @@ export declare function createTaskViews(runtime: TaskViewsRuntime): {
88
90
  progressOf: (taskRef: number, currentDocument: string | null) => MilestoneProgressView[] | null;
89
91
  resultDetailOf: (run: Run, who: Who, now: Date) => ResultDetail;
90
92
  };
93
+ /** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
94
+ * from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
95
+ export declare function shipViewOf(ship: ShipChecks | null, now: Date): NonNullable<BrowserTaskView["ship"]> | null;
96
+ /** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
97
+ * queued, and where it stands in one line. Null: this task is no release. */
98
+ export declare function releaseStepsViewOf(progress: ReturnType<typeof releaseProgressFor>, now: Date): NonNullable<BrowserTaskView["release"]> | null;
@@ -1,6 +1,8 @@
1
1
  import { feedbackImageProblem } from "../result-actions.js";
2
2
  import { parseFeedbackImage } from "../result-review.js";
3
3
  import { releaseViewOf } from "../release-view.js";
4
+ import { RELEASE_STEPS } from "../release.js";
5
+ import { releaseProgressFor, shipChecksForTask } from "../store.js";
4
6
  import { releaseCiRunningOf } from "../release-ci-hold.js";
5
7
  import { releaseObligationsOf } from "../release-recheck.js";
6
8
  import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
@@ -21,6 +23,7 @@ import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-r
21
23
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
22
24
  import { resultCheckLevel } from '../result-follow-ups.js';
23
25
  import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
26
+ import { archiveOf } from "../store.js";
24
27
  import { runActivityOf } from "../task-activity.js";
25
28
  import { taskFingerprint } from "../task-live.js";
26
29
  import { pullRequestFactOf } from '../task-status.js';
@@ -792,6 +795,10 @@ export function createTaskViews(runtime) {
792
795
  const presentedData = { ...data, rootId: family?.root.id ?? taskId, rootTitle: family?.root.title ?? data.task.title,
793
796
  ...(family === null || data.assignment != null ? {} : { history: familyHistory(family) }),
794
797
  versionLabel: family !== null && family.current.id !== taskId ? `Viewing ${family.versions.findIndex(one => one.id === taskId) === 0 ? "Original" : `Revision ${family.versions.findIndex(one => one.id === taskId)}`} · ${family.current.state === "running" ? "A newer revision is running" : "A newer revision is current"}` : null };
798
+ // Put away (v145, task-tidy.md): who or what put it away and why, and whether this person can put it away or restore it.
799
+ const archived = archiveOf(runtime.store, taskId);
800
+ presentedData.archived = archived;
801
+ presentedData.canPutAway = who.via === "cookie" && who.role === "approver";
795
802
  if (scopeDraft !== undefined)
796
803
  presentedData.scopeDraft = scopeDraft;
797
804
  if (cancelDraft !== undefined)
@@ -831,8 +838,12 @@ export function createTaskViews(runtime) {
831
838
  } })() } } : {}) } : page.workspace?.view;
832
839
  // A refusal that keeps a draft (a scope edit, an armed cancel) or asks for the plan editor reopens on Plan, where the
833
840
  // draft and its error are; anything else on Activity unless the address named a tab.
834
- const tab = extras.tab ?? (scopeDraft !== undefined || cancelDraft !== undefined || editPlan ? "plan" : "activity");
835
- const oneTaskView = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
841
+ const tab = extras.tab ?? "plan";
842
+ const onePage = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
843
+ // Ship (its pull request's checks, each on one time axis) and a release's own steps, read from the store now.
844
+ const rootId = family?.root.id ?? taskId;
845
+ const oneTaskView = onePage?.kind === "task" ? { ...onePage, ship: shipViewOf(shipChecksForTask(runtime.store, rootId), runtime.clock()),
846
+ release: releaseStepsViewOf(releaseProgressFor(runtime.store, rootId), runtime.clock()) } : onePage;
836
847
  // The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
837
848
  if (extras.result != null) {
838
849
  page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
@@ -1158,3 +1169,80 @@ function autoReleaseOf(scope) {
1158
1169
  return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
1159
1170
  : scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
1160
1171
  }
1172
+ /** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
1173
+ * from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
1174
+ export function shipViewOf(ship, now) {
1175
+ if (ship === null || ship.checks.length === 0)
1176
+ return null;
1177
+ const at = (iso) => iso === null ? null : Date.parse(iso);
1178
+ const observed = Date.parse(ship.observedAt);
1179
+ const end = Number.isFinite(observed) ? observed : now.getTime();
1180
+ const starts = ship.checks.map(one => at(one.startedAt)).filter((one) => one !== null && Number.isFinite(one));
1181
+ const origin = starts.length === 0 ? end : Math.min(...starts);
1182
+ const st = (state) => state === "passed" ? "ok" : state === "failed" || state === "cancelled" ? "bad" : state === "running" ? "run" : state === "queued" ? "q" : "ok";
1183
+ const rows = ship.checks.map(one => {
1184
+ const start = at(one.startedAt), done = at(one.completedAt);
1185
+ const from = start ?? origin;
1186
+ return { name: one.name, ...(one.workflow === null || one.workflow === one.name ? {} : { sub: one.workflow }),
1187
+ ...(one.state === "skipped" ? { sub: "skipped" } : one.state === "neutral" ? { sub: "neutral" } : {}),
1188
+ st: st(one.state), start: Math.max(0, (from - origin) / 1000), dur: start === null ? 0 : Math.max(0, ((done ?? end) - start) / 1000) };
1189
+ });
1190
+ const passed = ship.checks.filter(one => one.state === "passed" || one.state === "skipped" || one.state === "neutral").length;
1191
+ const failed = ship.checks.filter(one => one.state === "failed" || one.state === "cancelled").length;
1192
+ const running = ship.checks.length - passed - failed;
1193
+ // The overall status is the observation's own (the one merges read); the rows only time each check.
1194
+ // `none` (the pull request closed or merged, or GitHub reported no checks) is never painted as passed.
1195
+ const state = ship.observed === "failing" ? "failed" : ship.observed === "passing" ? "passed" : ship.observed === "running" ? "running" : "none";
1196
+ return {
1197
+ pr: ship.prNumber, head: ship.headSha.slice(0, 7), state, rows,
1198
+ summary: state === "failed" ? (failed > 0 ? `${failed} of ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} failed` : "Checks are failing on GitHub")
1199
+ : state === "running" ? `${passed} of ${ship.checks.length} checks passed` : state === "passed" ? `All ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} passed`
1200
+ : "No current checks: the pull request is closed or merged, or GitHub reported none",
1201
+ observedAt: ship.observedAt,
1202
+ };
1203
+ }
1204
+ /** A release step's name in words (release.ts RELEASE_STEPS). */
1205
+ const RELEASE_STEP_WORDS = {
1206
+ gate: "Release gate", approval: "Approval", check: "Release check", complete: "Complete the task", quiet: "Wait for quiet", deploy: "Deploy to this Mac",
1207
+ link: "Point the CLI at it", ci: "Pull request checks", merge: "Merge into main", tree: "Verify the merged tree", tag: "Tag", publish: "Publish to npm", homebrew: "Homebrew",
1208
+ };
1209
+ /** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
1210
+ * queued, and where it stands in one line. Null: this task is no release. */
1211
+ export function releaseStepsViewOf(progress, now) {
1212
+ if (progress === null)
1213
+ return null;
1214
+ const origin = Date.parse(progress.startedAt);
1215
+ const recorded = new Map(progress.steps.map(one => [one.step, one]));
1216
+ const all = RELEASE_STEPS.filter(step => step !== "done");
1217
+ // A release that started part way (one carried under an earlier approval, or resumed from an older journal) shows
1218
+ // the steps from where its own record starts; a step missing between two recorded ones is a write that was lost.
1219
+ const firstAt = all.findIndex(step => recorded.has(step));
1220
+ const lastAt = all.reduce((at, step, index) => recorded.has(step) ? index : at, -1);
1221
+ const steps = firstAt <= 0 ? all : all.slice(firstAt);
1222
+ const rows = steps.map(step => {
1223
+ const one = recorded.get(step);
1224
+ // A step with no record is never painted as done: between recorded ones it says so, after them it is queued.
1225
+ if (one === undefined)
1226
+ return all.indexOf(step) < lastAt ? { name: RELEASE_STEP_WORDS[step] ?? step, sub: "not recorded", st: "q", start: 0, dur: 0 }
1227
+ : { name: RELEASE_STEP_WORDS[step] ?? step, st: "q", start: 0, dur: 0 };
1228
+ const start = Date.parse(one.startedAt), end = one.endedAt === null ? now.getTime() : Date.parse(one.endedAt);
1229
+ const st = one.state === "done" ? "ok" : one.state === "failed" ? "bad" : one.state === "waiting" ? "wait" : "run";
1230
+ return { name: RELEASE_STEP_WORDS[step] ?? step, ...(one.detail === null || one.state === "done" ? {} : { sub: one.detail.slice(0, 160) }), st,
1231
+ start: Number.isFinite(start) && Number.isFinite(origin) ? Math.max(0, (start - origin) / 1000) : 0, dur: Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, (end - start) / 1000) : 0 };
1232
+ });
1233
+ const current = rows.findIndex(row => row.st === "run" || row.st === "wait" || row.st === "bad");
1234
+ // Released only on its own evidence: the last step (Homebrew, after publication) recorded done. A merge (authority
1235
+ // `merged`) is not a release: verifying the tree, the tag and publication still follow it.
1236
+ const done = progress.stopped === null && recorded.get("homebrew")?.state === "done";
1237
+ const at = current === -1 ? null : rows[current];
1238
+ const last = lastAt === -1 ? null : RELEASE_STEP_WORDS[all[lastAt]] ?? all[lastAt];
1239
+ return {
1240
+ tag: progress.tag, version: progress.version, pr: progress.pr,
1241
+ rows,
1242
+ state: progress.stopped !== null ? "failed" : done ? "done" : at?.st === "wait" ? "waiting" : "running",
1243
+ headline: done ? `Released ${progress.version}` : progress.stopped !== null ? `Stopped at ${RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step}`
1244
+ : at !== null ? `${at.name} · Step ${current + 1} of ${rows.length}` : last === null ? "Starting" : `${last} done · the next step starts on its own`,
1245
+ stopped: progress.stopped === null ? null : { step: RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step, message: progress.stopped.message, at: progress.stopped.at },
1246
+ startedAt: progress.startedAt,
1247
+ };
1248
+ }
@@ -2,6 +2,7 @@
2
2
  import { type Html } from "../html.js";
3
3
  import { type ServerResponse } from "node:http";
4
4
  import { type Artifact, type Run, type Store } from "../store.js";
5
+ import type { HandlerContext } from './handler-context.js';
5
6
  import type { ServerRuntime } from './runtime.js';
6
7
  import { type PeekAdmission } from "./render-pages.js";
7
8
  import { type CompletedWorkRow, type RankedReviewRow, type ReviewCockpitView } from "./render-results.js";
@@ -36,3 +37,20 @@ export declare function createTasksHandlers(runtime: ServerRuntime): {
36
37
  };
37
38
  liveRunIds: (rows: readonly (Pick<Run, "id" | "outcome" | "leaseId" | "taskRef">)[]) => Set<number>;
38
39
  };
40
+ /** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
41
+ * putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
42
+ * re-proves the person's standing inside its own transaction. */
43
+ export declare function createTidyHandlers(runtime: ServerRuntime): {
44
+ "work.put-away": (ctx: HandlerContext) => Promise<void>;
45
+ "work.put-away.apply": (ctx: HandlerContext) => Promise<void>;
46
+ "task.act.archive": (ctx: HandlerContext) => Promise<void>;
47
+ "task.act.restore": (ctx: HandlerContext) => Promise<void>;
48
+ };
49
+ /**
50
+ * Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
51
+ * Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
52
+ * address still answers itself.
53
+ */
54
+ export declare function retiredTasksAddress(route: "board" | "done", url: URL): string | null;
55
+ /** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
56
+ export declare const BOARD_COLUMN_LIMIT = 40;
@@ -1,5 +1,6 @@
1
1
  import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
2
- import { dropFeedbackImage, releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
2
+ import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
3
+ import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
3
4
  import { steerCapabilityOf } from "../steer-capabilities.js";
4
5
  import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
5
6
  /** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
@@ -10,7 +11,7 @@ function returnedFlow(value) {
10
11
  import { originOf } from "../task-source.js";
11
12
  import { ACP_AGENTS, isAcpProvider } from "../acp-agents.js";
12
13
  import { handlersOf } from './handler-registry.js';
13
- import { closedQuestionWords } from "../decision.js";
14
+ import { closedQuestionWords, LIMITS } from "../decision.js";
14
15
  import { DEPRECATED_PAGE } from "../deprecations.js";
15
16
  /** tasks handlers, moved without changing their route bodies. */
16
17
  import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
@@ -29,8 +30,11 @@ import { CodingActionError } from '../coding-workspace.js';
29
30
  import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
30
31
  import { hasForbiddenControls, validateNote } from "../decision.js";
31
32
  import { builderReadyFor, diagnoseTaskDispatch, firstRunBuilderRepo } from "../dispatch.js";
32
- import { withBuilderStep } from "../first-run.js";
33
- import { evidencePack, evidencePackHtml } from "../evidence-pack.js";
33
+ import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
34
+ import { newTaskPage } from "./render-new-task.js";
35
+ import { nextPage } from "./render-next.js";
36
+ import { evidencePack } from "../evidence-pack.js";
37
+ import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
34
38
  import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
35
39
  import { computeGaps } from "../gaps.js";
36
40
  import { limitsView } from "../limits-ui.js";
@@ -67,11 +71,11 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
67
71
  import { requestContext } from "./request-context.js";
68
72
  import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
69
73
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
70
- import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
74
+ import { putAwayPage, boardBody, boardWorkers, inboxFingerprints, inboxPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
71
75
  import { runEfficiencyOf } from "../run-efficiency.js";
72
76
  import { timingFor } from "../latency-timeline.js";
73
- import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
74
- import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
77
+ import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
78
+ import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
75
79
  import {} from "./session.js";
76
80
  export function createTasksHandlers(runtime) {
77
81
  const { identities, store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
@@ -250,7 +254,7 @@ export function createTasksHandlers(runtime) {
250
254
  // the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
251
255
  // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
252
256
  // current plan (one saved file, for live rows only), and without it the row names the step by number.
253
- work = { ...work, items: work.items.map((item) => {
257
+ const enrich = (page) => ({ ...page, items: page.items.map((item) => {
254
258
  if (item.liveRunId !== null) {
255
259
  const ref = store.lookupRef(item.activeTaskId);
256
260
  const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
@@ -269,15 +273,25 @@ export function createTasksHandlers(runtime) {
269
273
  return item;
270
274
  const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
271
275
  return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
272
- }) };
276
+ }) });
277
+ work = enrich(work);
278
+ // The Board view: the same index as four columns (needs you, queued, running, finished), never a second classifier.
279
+ const board = url.searchParams.get("layout") !== "board" ? null : (() => {
280
+ const column = (view) => enrich(workIndexPage(store, now, workAccess(), { view, limit: BOARD_COLUMN_LIMIT, project, root: evidenceRoot, ...(state === null ? {} : { state }) }));
281
+ return { needs: column("needs-you"), queued: column("queued"), running: column("running"), finished: column("completed") };
282
+ })();
273
283
  const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
274
284
  view,
285
+ lens: project,
286
+ all: url.searchParams.get("all") === "1",
287
+ board,
275
288
  ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
276
289
  work,
277
290
  previous: url.searchParams.has('cursor'),
278
291
  multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
279
292
  now,
280
293
  state,
294
+ canPutAway: who.via === "cookie" && who.role === "approver",
281
295
  // v105: subscription windows and monthly budgets, for whoever runs the installation.
282
296
  limits: store.isInstanceOperator(who.name)
283
297
  ? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
@@ -332,6 +346,9 @@ export function createTasksHandlers(runtime) {
332
346
  deliverable: ref?.deliverable ?? "branch",
333
347
  route,
334
348
  origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
349
+ // Whether a remembered password may approve these exact terms: the task page's own preview (task-views.ts
350
+ // approvalFresh), read without minting a second nonce. Display only; the approve POST proves it again.
351
+ remembered: nonce !== "" && runtime.taskViewData(item.approval.taskId, who, null, { mintNonce: false })?.approvalFresh != null,
335
352
  csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
336
353
  }));
337
354
  }
@@ -586,9 +603,12 @@ export function createTasksHandlers(runtime) {
586
603
  reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
587
604
  return sendScreen(response, 200, reviewPage);
588
605
  }
606
+ /** Finished tasks live in the Tasks list's Finished view now (UI revamp wave B): the old address keeps answering, as a
607
+ * GET alias, with a named project carried along. */
589
608
  async function showDone(ctx) {
590
- const { response, project } = ctx;
591
- return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
609
+ const { url, response } = ctx;
610
+ const named = url.searchParams.get("project");
611
+ return redirectGet(response, `/work?view=completed${named === null || named === "" ? "" : `&project=${encodeURIComponent(named)}`}`);
592
612
  }
593
613
  /** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
594
614
  * redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
@@ -622,7 +642,9 @@ export function createTasksHandlers(runtime) {
622
642
  const prefill = picked !== null && picked.kind === "task"
623
643
  ? { title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "), acceptance: acceptanceToLines(picked.acceptance).join("\n") }
624
644
  : null;
625
- return newTaskScreen(ctx, 200, null, prefill);
645
+ // An idea's link (or any ?text=) puts words in the box: bounded to what the box takes, nothing filed.
646
+ const text = ctx.url.searchParams.get("text");
647
+ return newTaskScreen(ctx, 200, null, prefill, text === null ? null : text.slice(0, 200));
626
648
  }
627
649
  async function showTask(ctx) {
628
650
  const { url, who, response } = ctx;
@@ -645,7 +667,8 @@ export function createTasksHandlers(runtime) {
645
667
  // One page per task (docs/plans/one-page-task-and-agent.md): the tab named, and the Result tab's result — the shown
646
668
  // version's own, or the exact run `run=` names, which must be a result of that very version.
647
669
  const tabWord = url.searchParams.get("tab");
648
- const tab = tabWord === "result" || tabWord === "plan" ? tabWord : "activity";
670
+ // The first tab (Plan, or Result once there is one) unless the address names Activity or Attempts.
671
+ const tab = tabWord === "result" || tabWord === "activity" || tabWord === "attempts" ? tabWord : "plan";
649
672
  const runWord = url.searchParams.get("run");
650
673
  const named = runWord === null ? null : /^[1-9][0-9]{0,14}$/.test(runWord) ? Number(runWord) : -1;
651
674
  if (named === -1)
@@ -693,22 +716,12 @@ export function createTasksHandlers(runtime) {
693
716
  "cache-control": "no-store", "x-content-type-options": "nosniff" });
694
717
  return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
695
718
  }
696
- return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
719
+ return sendScreen(response, 200, evidenceRecordPage(chromeFor(family.root.repo, "tasks"), pack));
697
720
  }
721
+ /** Build history lives on Agents now ("Finished in the last …"), and each task keeps its own attempts: the old address
722
+ * keeps answering, as a GET alias. */
698
723
  async function showRuns(ctx) {
699
- const { url, who, response, project } = ctx;
700
- const raw = url.searchParams.get("before");
701
- let before = null;
702
- if (raw !== null) {
703
- if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
704
- return refuse(response, who, 400, "that cursor is not a page", "/runs");
705
- }
706
- before = Number(raw);
707
- }
708
- const rows = store.listRunsBefore(before, RUNS_PAGE, project);
709
- const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
710
- const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
711
- return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
724
+ return redirectGet(ctx.response, "/agents");
712
725
  }
713
726
  async function showRun(ctx) {
714
727
  const { url, who, response, now, project } = ctx;
@@ -871,21 +884,12 @@ export function createTasksHandlers(runtime) {
871
884
  return redirect(ctx.response, "/flows");
872
885
  }
873
886
  async function showDecision(ctx) {
874
- const { url, who, response, now, project } = ctx;
887
+ const { url, who, response, now } = ctx;
875
888
  const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
876
889
  if (one === null)
877
890
  return noPage(who, response);
878
- const decision = store.getDecision(Number(one[1]));
879
- if (decision === null)
880
- return refuse(response, who, 404, "no such decision");
881
- const run = store.getRun(decision.run);
882
- if (run !== null && !visible(taskRepoOf(run.taskRef))) {
883
- return refuse(response, who, 404, "no such decision");
884
- }
885
- const taskId = taskOf(store, decision);
886
891
  const decisionReturn = url.searchParams.get("return");
887
- const back = decisionReturn === null ? null : safeChatReturn(decisionReturn);
888
- return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
892
+ return decisionScreen(ctx, Number(one[1]), decisionReturn === null ? null : safeChatReturn(decisionReturn), null, 200, now, null);
889
893
  }
890
894
  async function showDecisionEvidence(ctx) {
891
895
  const { url, who, response } = ctx;
@@ -1250,7 +1254,12 @@ export function createTasksHandlers(runtime) {
1250
1254
  // Irreversible options never ride one accidental tap: the form arms
1251
1255
  // them behind an explicit confirmation field, and the server checks —
1252
1256
  // the client rendering is convenience, this is the rule.
1257
+ // The question page's own form hears a refusal in its box, with nothing changed; /next keeps its refusal page.
1258
+ const inPlace = requestedReturn !== "next";
1259
+ const pageBack = requestedReturn === null ? null : decisionBack;
1253
1260
  if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
1261
+ if (inPlace)
1262
+ return decisionScreen(ctx, id, pageBack, `“${chosen.label}” can't be undone: tick the box to confirm it`, 400, now, { choice, note: body.get("note") });
1254
1263
  return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
1255
1264
  }
1256
1265
  const note = body.get("note");
@@ -1265,6 +1274,9 @@ export function createTasksHandlers(runtime) {
1265
1274
  const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
1266
1275
  const closed = answered.reason === "closed" ? store.getDecision(id)?.supersededReason ?? null : null;
1267
1276
  const why = closed !== null ? closedQuestionWords(closed) : answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1277
+ if (inPlace && answered.reason !== "closed" && answered.reason !== "already-answered") {
1278
+ return decisionScreen(ctx, id, pageBack, answered.reason === "bad-option" ? "Choose one of the options" : answered.reason === "bad-note" ? `The note is too long or can't be sent (up to ${LIMITS.note} characters)` : why, status, now, { choice, note });
1279
+ }
1268
1280
  return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
1269
1281
  }
1270
1282
  return redirect(response, decisionBack);
@@ -3057,7 +3069,24 @@ export function createTasksHandlers(runtime) {
3057
3069
  live.add(run.id);
3058
3070
  return live;
3059
3071
  }
3072
+ const tidyHandlers = createTidyHandlers(runtime);
3073
+ /** The question page, with a refusal in its box when an answer was refused (the box keeps the choice to make). */
3074
+ function decisionScreen(ctx, id, back, refusal, code, now, draft) {
3075
+ const { who, response, project } = ctx;
3076
+ const decision = store.getDecision(id);
3077
+ if (decision === null)
3078
+ return refuse(response, who, 404, "no such decision");
3079
+ const run = store.getRun(decision.run);
3080
+ if (run !== null && !visible(taskRepoOf(run.taskRef))) {
3081
+ return refuse(response, who, 404, "no such decision");
3082
+ }
3083
+ const taskId = taskOf(store, decision);
3084
+ const ref = taskId === "?" ? null : store.lookupRef(taskId);
3085
+ const place = { title: ref === null ? null : store.getTask(taskId)?.title ?? null, repo: ref?.repo ?? null, run: run === null ? null : { id: run.id, role: run.role } };
3086
+ return sendScreen(response, code, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), now, place, back, refusal, draft));
3087
+ }
3060
3088
  const registrations = handlersOf("tasks", {
3089
+ ...tidyHandlers,
3061
3090
  "code.page": showCode("workspace"),
3062
3091
  "code.session": showCode("session"),
3063
3092
  "code.state": showCode("state"),
@@ -3066,12 +3095,13 @@ export function createTasksHandlers(runtime) {
3066
3095
  "code.changes-read": showCode("changes", true),
3067
3096
  "code.ship-preview": showCode("ship", true),
3068
3097
  "code.other": showCode("unknown"),
3098
+ // Retired into Tasks and the task page (src/design/KIT.md): each old address still answers, as a redirect.
3069
3099
  "inbox": showInbox,
3070
3100
  "work": showWork,
3071
3101
  "next": showNext,
3072
3102
  "board": showBoard,
3073
3103
  "review": showReview,
3074
- "done": showDone,
3104
+ "done": async (ctx) => { const to = retiredTasksAddress("done", ctx.url); return to === null ? showDone(ctx) : redirect(ctx.response, to); },
3075
3105
  "tasks": showTasks,
3076
3106
  "queue": showQueue,
3077
3107
  "tasks.new": showNewTask,
@@ -3138,7 +3168,7 @@ export function createTasksHandlers(runtime) {
3138
3168
  });
3139
3169
  /** The add page, once more: blank, from a shipped template (`?template=`, as the old task list did), or with a refused
3140
3170
  * filing's own words and status (the draft keeps the project revision it was written against). */
3141
- function newTaskScreen(ctx, status, problem, draft) {
3171
+ function newTaskScreen(ctx, status, problem, draft, text = null) {
3142
3172
  const { who, response, project } = ctx;
3143
3173
  const chainable = store
3144
3174
  .listTasksScoped(project, undefined, 100, null)
@@ -3150,7 +3180,84 @@ export function createTasksHandlers(runtime) {
3150
3180
  chainable.unshift({ id: after, title: "(as submitted)" });
3151
3181
  const chrome = chromeFor(project, "tasks");
3152
3182
  const revision = draft?.values !== undefined ? undefined : who.via === "cookie" ? who.session.projectRevision : 0;
3153
- return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft));
3183
+ // Ideas for the project the box starts on: what the first run found there (issues, TODOs), else the safe generic
3184
+ // ones while the first run is open; none once someone has filed work and nothing was found.
3185
+ const startsOn = draft?.values?.get("repo") ?? project ?? chrome.projects?.[0]?.path ?? null;
3186
+ const found = startsOn === null ? null : runtime.firstTasks.get(startsOn)?.found ?? null;
3187
+ const ideas = found ?? (runtime.firstRunStepsNow(ctx.now) !== null && !store.hasAnyWork() ? firstTaskSuggestions({ issues: [], todos: [] }) : []);
3188
+ return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas));
3154
3189
  }
3155
3190
  return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3156
3191
  }
3192
+ /** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
3193
+ * putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
3194
+ * re-proves the person's standing inside its own transaction. */
3195
+ export function createTidyHandlers(runtime) {
3196
+ const { store, sendScreen, chromeFor, authenticateApprover, workAccess, bustBadge } = runtime;
3197
+ async function showPutAway(ctx) {
3198
+ const { url, who, response, now, project } = ctx;
3199
+ const days = tidyDays(url.searchParams.get("days"));
3200
+ if (days === null)
3201
+ return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/work/put-away");
3202
+ const releases = releaseFactsOf(store);
3203
+ const items = tidyPreview(store, now, { releases, days, access: workAccess() });
3204
+ return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
3205
+ problem: url.searchParams.get("problem"), releasesSkipped: releases.problem }));
3206
+ }
3207
+ async function applyPutAway(ctx) {
3208
+ const { who, response, now, posted } = ctx;
3209
+ const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
3210
+ const days = tidyDays(body.get("days"));
3211
+ const back = (problem) => redirect(response, `/work/put-away?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
3212
+ if (days === null)
3213
+ return back("Choose a number of days from 1 to 365.");
3214
+ if (!authenticateApprover(who, body.get("password") ?? "").ok)
3215
+ return back("Enter your Toolroll password to put these away.");
3216
+ const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
3217
+ if (!done.ok)
3218
+ return back(done.message);
3219
+ bustBadge();
3220
+ return redirect(response, "/work?view=archived");
3221
+ }
3222
+ const oneTask = (kind) => async (ctx) => {
3223
+ const { url, who, response, now, posted } = ctx;
3224
+ const act = matchTaskPath(url.pathname, `/${kind}$`);
3225
+ if (act === null)
3226
+ return refuse(response, who, 404, "There's no page at this address.", "/work");
3227
+ const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
3228
+ const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
3229
+ const done = kind === "archive"
3230
+ ? archiveTaskFamily(store, act.taskId, { by: who.name, byKind: "person", rule: null, reason: (body.get("reason") ?? "").trim() || "Put away by hand" }, now, { guard: standing })
3231
+ : restoreTaskFamily(store, act.taskId, who.name, now, standing);
3232
+ if (!done.ok)
3233
+ return refuse(response, who, done.reason === "unknown-task" ? 404 : 409, done.message, taskHref(act.taskId));
3234
+ bustBadge();
3235
+ const to = safeReturn(body.get("return"));
3236
+ return redirect(response, to === "/" ? taskHref(act.taskId) : to);
3237
+ };
3238
+ return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
3239
+ }
3240
+ /** A retired page's GET alias: a plain 302 (the shared 303 helper belongs to POST landings), as pages.ts morningAlias. */
3241
+ function redirectGet(response, to) {
3242
+ response.writeHead(302, { ...SAFETY, Location: to });
3243
+ response.end();
3244
+ }
3245
+ /**
3246
+ * Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
3247
+ * Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
3248
+ * address still answers itself.
3249
+ */
3250
+ export function retiredTasksAddress(route, url) {
3251
+ // (The Board keeps its own page for now: Tasks' Board view is where the app links; see the wave A report.)
3252
+ const project = url.searchParams.get("project");
3253
+ const keep = project === null || url.searchParams.getAll("project").length !== 1 ? "" : `project=${encodeURIComponent(project)}&`;
3254
+ switch (route) {
3255
+ case "done": return `/work?${keep}view=completed`;
3256
+ case "board":
3257
+ if (url.searchParams.get("view") === "order" || url.searchParams.get("fragment") === "1")
3258
+ return null;
3259
+ return url.searchParams.get("scope") === "all" ? "/work?all=1&layout=board" : `/work?${keep}layout=board`;
3260
+ }
3261
+ }
3262
+ /** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
3263
+ export const BOARD_COLUMN_LIMIT = 40;
@@ -78,7 +78,7 @@ export function createTerminalHandlers(runtime, deps) {
78
78
  const id = TERMINAL_ID.exec(url.pathname)?.[1] ?? "";
79
79
  const nonce = randomBytes(16).toString("base64");
80
80
  const host = request.headers.host ?? "";
81
- const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>Live — Toolroll</title><link rel="icon" href="/icon.svg"><link rel="manifest" href="/manifest.webmanifest"><link rel="stylesheet" href="/assets/terminal.css"></head><body><div id="so-term"></div><div id="so-term-boot" hidden data-csrf="${who.session.csrf}" data-session="${id}" data-passwordless="${passwordlessOn(store) ? "1" : ""}"></div><script type="module" src="/assets/terminal.js" nonce="${nonce}"></script></body></html>`;
81
+ const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>Terminals — Toolroll</title><link rel="icon" href="/icon.svg"><link rel="manifest" href="/manifest.webmanifest"><link rel="stylesheet" href="/assets/terminal.css"></head><body><div id="so-term"></div><div id="so-term-boot" hidden data-csrf="${who.session.csrf}" data-session="${id}" data-passwordless="${passwordlessOn(store) ? "1" : ""}"></div><script type="module" src="/assets/terminal.js" nonce="${nonce}"></script></body></html>`;
82
82
  response.writeHead(200, {
83
83
  "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer",
84
84
  // The socket is this page's own host: named outright, since older WebKit doesn't read 'self' as ws:/wss:.
@@ -0,0 +1,22 @@
1
+ import { type FirstRunStep } from "../first-run.js";
2
+ import { type CheckStep } from "./kit.js";
3
+ import type { ServerRuntime } from "./runtime.js";
4
+ import { type Chrome, type Screen } from "./chrome.js";
5
+ export type WelcomeFacts = {
6
+ steps: readonly FirstRunStep[];
7
+ projects: number;
8
+ /** This person has a live push enrollment on some device. */
9
+ alerts: boolean;
10
+ };
11
+ /** The five steps as the page shows them, and which one leads. */
12
+ export declare function welcomeSteps(facts: WelcomeFacts): {
13
+ steps: CheckStep[];
14
+ primary: {
15
+ label: string;
16
+ href: string;
17
+ } | null;
18
+ };
19
+ export declare function welcomePage(chrome: Chrome, facts: WelcomeFacts): Screen;
20
+ export declare function createWelcomeHandlers(runtime: ServerRuntime): {
21
+ registrations: import("./handler-registry.js").Registration[];
22
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The first run's own page, /welcome (Wave E, src/design/KIT.md): five guided steps to a first result, each derived
3
+ * from live facts on every render (never a stored cursor), each with the one action that does it. The next required
4
+ * step's action is the screen's one primary (on a fresh install: Add a project); alerts on the phone are optional and
5
+ * never lead. It only reads: every step's action is a link to the page that does it. Once the first result has
6
+ * arrived (or for a project-limited account, or in the demo) the first run is over and the address leads Home.
7
+ */
8
+ import { builderReadyFor, firstRunBuilderRepo } from "../dispatch.js";
9
+ import { withBuilderStep } from "../first-run.js";
10
+ import { html } from "../html.js";
11
+ import { START_BUILDER_HREF } from "../needs-you.js";
12
+ import { handlersOf } from "./handler-registry.js";
13
+ import { redirect } from "./http.js";
14
+ import { btn, checklist, dock, pageHeader } from "./kit.js";
15
+ import { screen } from "./chrome.js";
16
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
17
+ /** The five steps as the page shows them, and which one leads. */
18
+ export function welcomeSteps(facts) {
19
+ const step = (key) => facts.steps.find(one => one.key === key);
20
+ const agent = step("agent"), builder = step("builder"), task = step("task");
21
+ const projectDone = facts.projects > 0;
22
+ const agentDone = (agent?.done ?? false) && (builder?.done ?? true);
23
+ const command = agent?.action?.kind === "command" ? agent.action.command : null;
24
+ const agentState = agentDone ? "done" : agent?.checking === true ? "checking" : "open";
25
+ const agentText = agentDone ? "An agent is signed in and a builder is ready."
26
+ : agent?.checking === true ? "Checking for a signed-in agent on this computer…"
27
+ : agent !== undefined && !agent.done && command !== null ? html `On the computer running Toolroll, run <code class="k-mono">${command}</code>.`
28
+ : "Start a builder so tasks can run.";
29
+ const agentLink = agentDone || agent?.checking === true ? null
30
+ : agent !== undefined && !agent.done ? { label: "Agent settings", href: "/settings/models" } : { label: "Start a builder", href: START_BUILDER_HREF };
31
+ const taskDone = task?.done ?? false;
32
+ // The next required step leads (alerts are optional and never do).
33
+ const order = [
34
+ { key: "project", open: !projectDone, link: { label: "Add a project", href: "/projects" } },
35
+ { key: "agent", open: agentState === "open", link: agentLink },
36
+ { key: "task", open: !taskDone && projectDone, link: { label: "New task", href: "/tasks/new" } },
37
+ ];
38
+ const lead = order.find(one => one.open && one.link !== null) ?? null;
39
+ const action = (key, link) => link === null ? null : btn(link.label, { sm: true, href: link.href, ...(lead?.key === key ? { variant: "primary", docked: true } : {}) });
40
+ const steps = [
41
+ { key: "password", title: "Your password", text: "You're signed in. Your first login is saved on the computer running Toolroll.", state: "done" },
42
+ { key: "project", title: "Add a project", text: projectDone ? plural(facts.projects, "project added.", "projects added.") : "A git repository agents work in.", state: projectDone ? "done" : "open", action: action("project", order[0].link) },
43
+ { key: "agent", title: "Agent and models", text: agentText, state: agentState, action: action("agent", agentLink) },
44
+ { key: "phone", title: "Alerts on your phone", text: facts.alerts ? "Alerts are on." : "Optional. Hear when something needs you.", state: facts.alerts ? "done" : "open", action: btn("Set up alerts", { sm: true, href: "/settings/alerts#this-device" }) },
45
+ { key: "task", title: "Your first task", text: taskDone ? "Filed. Its plan waits for your approval." : "Describe a change and approve its plan.", state: taskDone ? "done" : "open",
46
+ action: projectDone ? action("task", order[2].link) : null },
47
+ ];
48
+ return { steps, primary: lead?.link ?? null };
49
+ }
50
+ export function welcomePage(chrome, facts) {
51
+ const { steps, primary } = welcomeSteps(facts);
52
+ const left = steps.filter(one => one.state !== "done").length;
53
+ const body = html `${pageHeader("Welcome")}<div class="k-doc"><div class="k-hello"><h2>Get to your first result.</h2><p>${left === 0 ? "Setup is done." : `${plural(left, "step", "steps")} left.`}</p></div>\
54
+ ${checklist(steps, "Setup steps")}</div>${dock(primary === null ? null : btn(primary.label, { variant: "primary", href: primary.href }))}`;
55
+ return screen("Welcome", body, { chrome });
56
+ }
57
+ export function createWelcomeHandlers(runtime) {
58
+ const { store, sendScreen, chromeFor, firstRunStepsNow, managedRepos, visible } = runtime;
59
+ async function showWelcome(ctx) {
60
+ const { who, response, now, project } = ctx;
61
+ const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now));
62
+ // The first run is over (a first result, a project-limited account, the demo): the address leads Home.
63
+ if (steps === null)
64
+ return redirect(response, "/");
65
+ const alerts = who.via === "cookie" && store.listPushSubscriptions(who.name).some(one => one.retiredAt === null);
66
+ return sendScreen(response, 200, welcomePage(chromeFor(project, "work"), { steps, projects: managedRepos().length, alerts }));
67
+ }
68
+ return { registrations: handlersOf("tasks", { welcome: showWelcome }) };
69
+ }