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
package/dist/release.js CHANGED
@@ -81,6 +81,13 @@ export const REQUIRED_PR_CHECKS_WORKFLOW = {
81
81
  export const PROTECTION_REPORT_ONLY_CHECKS = [];
82
82
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
83
83
  export const PROTECTION_MINIMUM_CHECKS = REQUIRED_PR_CHECKS.filter(name => !PROTECTION_REPORT_ONLY_CHECKS.includes(name));
84
+ /**
85
+ * Pull-request checks a release never waits for or fails on, by exact name and the workflow file that defines them:
86
+ * youiyoux's UI drift check (src/design/KIT.md), which fails a pull request that adds design drift but says nothing
87
+ * about whether a build is safe to release. Only a GitHub Actions run of that name from that workflow is left out; the
88
+ * same name from anywhere else is judged like any other check. Never a required check.
89
+ */
90
+ export const ADVISORY_PR_CHECKS = { "youiyoux · UI drift": ".github/workflows/youiyoux.yml" };
84
91
  /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
85
92
  * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
86
93
  * the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
@@ -130,7 +137,7 @@ export function landedWords(landed) {
130
137
  }
131
138
  const branchFile = (branch) => branch.replace(/[^A-Za-z0-9._-]+/g, "_");
132
139
  export const journalFile = (stateDir, branch) => join(stateDir, "releases", `${branchFile(branch)}.json`);
133
- export const resumeCommand = (branch, checkout) => `toolroll release ${branch} --repo ${checkout}`;
140
+ export const resumeCommand = (branch, checkout) => `toolroll release ${releaseBranchArg(branch)} --repo ${checkout}`;
134
141
  /** Whole or not at all: a private temp file, synced, renamed into place, its folder synced. */
135
142
  export function writeJournal(file, journal) {
136
143
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
@@ -232,7 +239,18 @@ export async function runRelease(options, adapters) {
232
239
  let freshApproval = false;
233
240
  let supersededTask = null;
234
241
  const save = () => { journal.updatedAt = adapters.now().toISOString(); writeJournal(file, journal); };
235
- const advance = (next) => { journal.step = next; journal.stopped = null; save(); };
242
+ /** Tell the progress record about a step (v146). Never throws and never waits: the release goes on whatever it says. */
243
+ const mark = (at, state, detail = null) => {
244
+ const j = journal;
245
+ if (j === null || adapters.progress === undefined || at === "done")
246
+ return;
247
+ try {
248
+ adapters.progress({ taskId: j.task.id, tag: j.tag, version: j.packageVersion, pr: j.pr, step: at, state, at: adapters.now(), detail });
249
+ }
250
+ catch { /* best effort */ }
251
+ };
252
+ const advance = (next) => { const was = journal.step; journal.step = next; journal.stopped = null; save(); if (was !== next)
253
+ mark(was, "done"); };
236
254
  const intend = (at) => { journal.intents[at] = adapters.now().toISOString(); save(); };
237
255
  const recoverDeploy = async () => {
238
256
  const j = journal;
@@ -266,13 +284,18 @@ export async function runRelease(options, adapters) {
266
284
  unreachable = networkWords(error);
267
285
  answer = null;
268
286
  }
269
- if (answer !== null)
287
+ if (answer !== null) {
288
+ if (said && journal !== null)
289
+ mark(journal.step, "running");
270
290
  return answer;
291
+ }
271
292
  if (adapters.now().getTime() >= deadline)
272
293
  throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes${unreachable === null ? "" : ` (GitHub last didn't answer: ${unreachable})`}.`);
273
294
  if (!said) {
274
295
  say(` waiting for ${what} (up to ${limits[at]} min)`);
275
296
  said = true;
297
+ if (journal !== null)
298
+ mark(journal.step, "waiting", `waiting for ${what}`);
276
299
  }
277
300
  await adapters.sleep(pollMs);
278
301
  }
@@ -584,6 +607,8 @@ export async function runRelease(options, adapters) {
584
607
  const watch = async (action) => { if (rel !== undefined)
585
608
  await identity(action, true); };
586
609
  const restart = async (head) => {
610
+ // The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
611
+ mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
587
612
  const tree = await adapters.repo.tree(head);
588
613
  if (rel !== undefined && j.authority && epoch !== null) {
589
614
  say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
@@ -663,7 +688,7 @@ export async function runRelease(options, adapters) {
663
688
  }
664
689
  const verdict = prChecksVerdict(runs, j.candidate.sha, REQUIRED_PR_CHECKS, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
665
690
  if (verdict.state === "failed") {
666
- throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\`.`);
691
+ throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${releaseBranchArg(j.branch)} --repo ${j.checkout} --new\`.`);
667
692
  }
668
693
  if (verdict.state !== "passed")
669
694
  return false;
@@ -739,6 +764,7 @@ export async function runRelease(options, adapters) {
739
764
  while (j.step !== "done" && homebrewLeft === null) {
740
765
  step = j.step;
741
766
  say(`• ${j.step}`);
767
+ mark(j.step, "running");
742
768
  try {
743
769
  switch (j.step) {
744
770
  case "gate": {
@@ -1217,6 +1243,7 @@ export async function runRelease(options, adapters) {
1217
1243
  save();
1218
1244
  if (progress.state === "pending" || progress.state === "failed") {
1219
1245
  homebrewLeft = { state: progress.state, why: progress.why };
1246
+ mark("homebrew", progress.state === "failed" ? "failed" : "waiting", progress.why);
1220
1247
  break;
1221
1248
  }
1222
1249
  advance("done");
@@ -1254,6 +1281,7 @@ export async function runRelease(options, adapters) {
1254
1281
  save();
1255
1282
  }
1256
1283
  catch { /* the stop is still reported */ }
1284
+ mark(journal.step, "failed", stop.message);
1257
1285
  }
1258
1286
  return { ok: false, reason: stop.reason, message: stop.message, step: journal?.step ?? step, journal, file, resume };
1259
1287
  }
@@ -1441,6 +1469,10 @@ export function prChecksVerdict(runs, head, required, options = {}) {
1441
1469
  const failed = [], waiting = [], failures = [];
1442
1470
  const valid = [];
1443
1471
  for (const run of runs) {
1472
+ // youiyoux's drift check from its own workflow neither holds nor fails a release (ADVISORY_PR_CHECKS).
1473
+ const advisory = Object.hasOwn(ADVISORY_PR_CHECKS, run.name) ? ADVISORY_PR_CHECKS[run.name] : undefined;
1474
+ if (advisory !== undefined && !required.includes(run.name) && run.app === "github-actions" && workflowIs(run.workflowPath, advisory))
1475
+ continue;
1444
1476
  if (run.head !== head) {
1445
1477
  failed.push(`${run.name} (reported for ${run.head == null ? "no commit" : run.head.slice(0, 7)}, not ${head.slice(0, 7)})`);
1446
1478
  continue;
@@ -1577,3 +1609,7 @@ export function workflowIs(path, file) {
1577
1609
  const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
1578
1610
  return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
1579
1611
  }
1612
+ /** The words `toolroll release` reads as its own (release-cli.ts), never as a branch: such a branch is named with --branch. */
1613
+ const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"]);
1614
+ /** How a person names this branch to `toolroll release` again. */
1615
+ export function releaseBranchArg(branch) { return RELEASE_WORDS.has(branch) ? `--branch ${branch}` : branch; }
package/dist/releases.js CHANGED
@@ -129,7 +129,7 @@ export function notifySecurityRelease(store, release, current, updateCommand, no
129
129
  dedupeKey: `release:security:${newer.version}:${recipient}`,
130
130
  kind: "security-release",
131
131
  pushClass: "attention",
132
- link: "/settings#updates",
132
+ link: "/settings/system#updates",
133
133
  subject: `Toolroll ${newer.version} is a security release`,
134
134
  body: `This computer runs ${current}. Update with \`${updateCommand}\`. Notes: ${newer.url}`,
135
135
  recipient,
@@ -5,8 +5,10 @@
5
5
  */
6
6
  import type { ApiTokenRow, Database } from "./store.js";
7
7
  import { type Html } from "./html.js";
8
- export declare const REQUEST_LIMITS_CSS: string;
8
+ /** The old card look; the section is built from the kit now. Kept for chrome.ts, which still concatenates it. */
9
+ export declare const REQUEST_LIMITS_CSS = "";
9
10
  /** "120 requests a minute · 10,000 a day" for one token's line. */
10
11
  export declare function tokenLimitWords(db: Database, token: ApiTokenRow): string;
11
- /** The operator's disclosure: everyone's tokens, or one token. Blank fields use the default shown. */
12
+ /** The operator's section: everyone's tokens, or one token. Blank fields use the default shown. Saving takes the
13
+ * operator's password (or Passwordless, or a fresh identity-provider sign-in). */
12
14
  export declare function requestLimitsHtml(db: Database, tokens: readonly ApiTokenRow[], now: number): Html;
@@ -1,31 +1,42 @@
1
- import { html, postForm, reauth } from "./html.js";
1
+ import { html, joinHtml } from "./html.js";
2
+ import { btn, disclose, formRow, input, note, select, settingsSection } from "./server/kit.js";
3
+ import { kitSectionForm } from "./settings-kit.js";
2
4
  import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
3
5
  const n = (value) => value.toLocaleString("en-US");
4
- export const REQUEST_LIMITS_CSS = `.credentials .limits{display:grid;gap:14px}.credentials .limits form{display:grid;gap:10px}.credentials .limits label{display:grid;gap:6px}` +
5
- `.credentials .limits input,.credentials .limits select{box-sizing:border-box;width:100%;min-width:0}.credentials .limits .pair{display:grid;grid-template-columns:1fr 1fr;gap:10px}.credentials .limits .actions{display:flex;flex-wrap:wrap;gap:8px}` +
6
- `.credentials .limits h3{font-size:.95rem;margin:4px 0 0}@media(max-width:600px){.credentials .limits .pair{grid-template-columns:1fr}.credentials .limits input,.credentials .limits select{font-size:16px}}`;
6
+ /** The old card look; the section is built from the kit now. Kept for chrome.ts, which still concatenates it. */
7
+ export const REQUEST_LIMITS_CSS = "";
7
8
  /** "120 requests a minute · 10,000 a day" for one token's line. */
8
9
  export function tokenLimitWords(db, token) {
9
10
  const limits = effectiveLimits(db, token.id, token.access);
10
11
  return `${n(limits.perMinute)} requests a minute · ${n(limits.perDay)} a day`;
11
12
  }
12
- /** The operator's disclosure: everyone's tokens, or one token. Blank fields use the default shown. */
13
+ /** The operator's section: everyone's tokens, or one token. Blank fields use the default shown. Saving takes the
14
+ * operator's password (or Passwordless, or a fresh identity-provider sign-in). */
13
15
  export function requestLimitsHtml(db, tokens, now) {
14
16
  const overrides = limitOverrides(db);
15
17
  const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
16
- const field = (name, label, value, placeholder, max) => html `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
17
- const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}`;
18
- const buttons = html `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
18
+ const number = (id, name, label, value, placeholder, max) => input({ id, name, type: "number", value: value === null ? "" : String(value), attrs: { min: 1, max, step: 1, inputmode: "numeric", placeholder, "aria-label": label } });
19
+ const installation = kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-everyone", gate: true, hidden: { target: "*", action: "save" },
20
+ save: { label: "Save limits" }, confirm: "Change request limits for everyone's tokens",
21
+ body: joinHtml([
22
+ formRow("Read tokens, a minute", `Default ${n(REQUEST_BUDGET_DEFAULTS.read)}.`, number("limits-read", "read-per-minute", "Read tokens, a minute", everyone.readPerMinute, String(REQUEST_BUDGET_DEFAULTS.read), PER_MINUTE_MAX)),
23
+ formRow("Act tokens, a minute", `Default ${n(REQUEST_BUDGET_DEFAULTS.act)}.`, number("limits-act", "act-per-minute", "Act tokens, a minute", everyone.actPerMinute, String(REQUEST_BUDGET_DEFAULTS.act), PER_MINUTE_MAX)),
24
+ formRow("Each token, a day", `Default ${n(REQUEST_BUDGET_DEFAULTS.day)}.`, number("limits-day", "per-day", "Each token, a day", everyone.perDay, String(REQUEST_BUDGET_DEFAULTS.day), PER_DAY_MAX)),
25
+ ]) });
26
+ const reset = kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-everyone-clear", hidden: { target: "*", action: "clear" },
27
+ save: { label: "Use defaults for everyone", variant: "outline" }, confirm: "Put everyone's tokens back on the default limits", body: html `` });
19
28
  const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
20
- const installation = postForm("/settings/request-limits", html `\
21
- <h3>Everyone's tokens</h3><div class="pair">${field("read-per-minute", "Read tokens, a minute", everyone.readPerMinute, String(REQUEST_BUDGET_DEFAULTS.read), PER_MINUTE_MAX)}\
22
- ${field("act-per-minute", "Act tokens, a minute", everyone.actPerMinute, String(REQUEST_BUDGET_DEFAULTS.act), PER_MINUTE_MAX)}</div>\
23
- ${field("per-day", "Each token, a day", everyone.perDay, String(REQUEST_BUDGET_DEFAULTS.day), PER_DAY_MAX)}${password}${buttons}`, { hidden: { target: "*" } });
24
- const one = live.length === 0 ? "" : postForm("/settings/request-limits", html `\
25
- <h3>One token</h3><label>Token<select name="target">${live.map(row => {
26
- const set = overrides.get(row.id);
27
- return html `<option value="${row.id}">${row.name} (${row.account}, ${row.access === "act" ? "act" : "read"})${set === undefined ? "" : " · own limits"}</option>`;
28
- })}</select></label>\
29
- <div class="pair">${field("per-minute", "A minute", null, "Everyone's", PER_MINUTE_MAX)}${field("per-day", "A day", null, "Everyone's", PER_DAY_MAX)}</div>${password}${buttons}`);
30
- return html `<details class="card"><summary>Request limits</summary><div class="limits">${installation}${one}</div></details>`;
29
+ const one = live.length === 0 ? "" : disclose("Limits for one token", kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-one",
30
+ save: { label: "Save limits", name: "action", value: "save" }, confirm: "Change one token's request limits",
31
+ body: joinHtml([
32
+ formRow("Token", null, select({ id: "limits-target", name: "target", label: "Token", wide: true, items: live.map(row => {
33
+ const set = overrides.get(row.id);
34
+ return [row.id, `${row.name} (${row.account}, ${row.access === "act" ? "act" : "read"})${set === undefined ? "" : " · own limits"}`];
35
+ }) })),
36
+ formRow("A minute", "Blank uses everyone's.", number("limits-one-minute", "per-minute", "A minute", null, "Everyone's", PER_MINUTE_MAX)),
37
+ formRow("A day", "Blank uses everyone's.", number("limits-one-day", "per-day", "A day", null, "Everyone's", PER_DAY_MAX)),
38
+ html `<div class="k-acts">${btn("Use everyone's limits", { sm: true, type: "submit", attrs: { name: "action", value: "clear" } })}</div>`,
39
+ ]) }));
40
+ return settingsSection({ id: "request-limits", title: "Request limits", lead: "How many requests each API token may make. A token over its limit is refused until the minute or day resets, and nothing it asked for runs.",
41
+ body: joinHtml([installation, reset, one, note("Only an instance operator sees and changes these.")]) });
31
42
  }
@@ -2,6 +2,7 @@ import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { refArchived } from "./task-archive.js";
5
6
  import { belongsToRelease } from "./release-authority.js";
6
7
  import { run as execRun } from "./exec.js";
7
8
  import { hardenedGit } from "./teardown-git.js";
@@ -126,6 +127,9 @@ function checkTarget(store, runId, level) {
126
127
  return refuse("no-commit", "This result has no finished commit to check.");
127
128
  if (ref.repo === null)
128
129
  return refuse("unplaced", "This result isn't in a project, so there is no check to run.");
130
+ // A put-away task (v145) runs no checks: asked for or already waiting, it is refused until restored.
131
+ if (refArchived(store, ref.id))
132
+ return refuse("archived", "This task is put away. Restore it first.");
129
133
  const chosen = checkCommandFor(store, ref.repo, level);
130
134
  if (chosen.command === null)
131
135
  return refuse("no-check", `${ref.repo} has no approved ${level === "quick" ? "quick or full" : "full"} check. Set one in Settings → Projects → Checks.`);
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Settings → Retention (v105): how long each kind of data is kept, and what
3
3
  * the next daily sweep would remove. An instance operator's page; changes
4
- * take a step-up.
4
+ * take a step-up. Built from the revamp kit; the Settings frame adds the title.
5
5
  */
6
6
  import { type Html } from "./html.js";
7
7
  import { type RetentionCount, type RetentionKind, type RetentionPeriods } from "./retention.js";
8
- export declare const RETENTION_CSS: string;
8
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
9
+ export declare const RETENTION_CSS = "";
9
10
  /** `chosen`: the kinds someone chose a period for; the rest keep their default. */
10
11
  export type RetentionView = {
11
12
  periods: RetentionPeriods;
@@ -14,7 +15,7 @@ export type RetentionView = {
14
15
  lastSweep: string | null;
15
16
  csrf: string;
16
17
  };
17
- export declare function retentionHtml(view: RetentionView, notice: {
18
+ export declare function retentionHtml(view: RetentionView, said: {
18
19
  said?: string | null;
19
20
  problem?: string | null;
20
21
  }): Html;
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * Settings → Retention (v105): how long each kind of data is kept, and what
3
3
  * the next daily sweep would remove. An instance operator's page; changes
4
- * take a step-up.
4
+ * take a step-up. Built from the revamp kit; the Settings frame adds the title.
5
5
  */
6
- import { html, postForm, reauth } from "./html.js";
6
+ import { html, joinHtml } from "./html.js";
7
7
  import { bytesWords } from "./storage.js";
8
8
  import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
9
- export const RETENTION_CSS = `.retention{max-width:720px;min-width:0}.retention-next{margin:0 0 16px}` +
10
- `.retention form{display:grid;gap:0}.retention .kind{display:grid;grid-template-columns:minmax(0,1fr) 11rem;gap:4px 16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--border)}` +
11
- `.retention .kind .name{font-weight:500;font-size:.875rem}.retention .kind .meta{grid-column:1;margin:0}.retention .kind select{grid-column:2;grid-row:1/span 2;width:100%;min-height:44px}` +
12
- `.retention .step-up{display:grid;gap:4px;font-size:.8125rem;margin:16px 0 8px;max-width:20rem}.retention .step-up input{min-height:44px}.retention button{justify-self:start;min-height:44px}` +
13
- `@media (max-width:560px){.retention .kind{grid-template-columns:minmax(0,1fr)}.retention .kind select{grid-column:1;grid-row:auto}}`;
9
+ import { formRow, infoLine, note, notice, select, settingsSection } from "./server/kit.js";
10
+ import { kitSectionForm } from "./settings-kit.js";
11
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
12
+ export const RETENTION_CSS = "";
14
13
  const listWords = (parts) => parts.length < 2 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
15
14
  /** The next sweep in a line: what goes and about how much space, or that nothing is due. */
16
15
  function nextWords(view) {
@@ -22,14 +21,18 @@ function nextWords(view) {
22
21
  const bytes = due.reduce((sum, one) => sum + one.bytes, 0);
23
22
  return `The next daily sweep removes ${listWords(due.map(countWords))} (about ${bytesWords(bytes)}).`;
24
23
  }
25
- export function retentionHtml(view, notice) {
26
- const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
24
+ export function retentionHtml(view, said) {
27
25
  const rows = RETENTION_KINDS.map(({ kind, label, detail }) => {
28
26
  const current = view.periods[kind];
29
27
  const chosen = view.chosen.includes(kind);
30
- const options = periodChoices(kind, current).map(days => html `<option value="${days === null ? "forever" : days}"${days === current ? html ` selected` : ""}>${periodLabel(kind, days, chosen || days !== current)}</option>`);
31
- return html `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${label}</label><p class="meta">${detail}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
28
+ const items = periodChoices(kind, current).map(days => [days === null ? "forever" : String(days), periodLabel(kind, days, chosen || days !== current)]);
29
+ return formRow(label, detail, select({ id: `keep-${kind}`, name: kind, label, current: current === null ? "forever" : String(current), items }), "inline");
32
30
  });
33
31
  const last = view.lastSweep === null ? "" : ` Last sweep ${view.lastSweep.slice(0, 10)}.`;
34
- return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Save</button>`)}<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
32
+ return settingsSection({ id: "retention", title: "How long data is kept", lead: html `<span data-retention-next>${nextWords(view)}${last}</span>`,
33
+ body: joinHtml([
34
+ said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "",
35
+ kitSectionForm({ proof: "retention", action: "/settings/retention", id: "retention-form", gate: true, body: joinHtml(rows) }),
36
+ note("Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold)."),
37
+ ]) });
35
38
  }
package/dist/scope.d.ts CHANGED
@@ -597,6 +597,11 @@ export type ApproveResult = {
597
597
  | {
598
598
  ok: false;
599
599
  reason: "machine-proof";
600
+ }
601
+ /** v145 (task-tidy.md): the task is put away; it is restored before anyone can approve it. */
602
+ | {
603
+ ok: false;
604
+ reason: "archived";
600
605
  };
601
606
  /**
602
607
  * Whether this name-and-token pair is a person the store knows. Shared by
@@ -999,3 +1004,6 @@ export declare function releaseApprovalOf(store: Store, taskId: string, _now?: D
999
1004
  ok: false;
1000
1005
  why: string;
1001
1006
  };
1007
+ /** When a password typed at `at` (ISO) stops standing in, for a page's words (ISO); null when `at` isn't a time. Words
1008
+ * only: freshnessFor decides whether it stands in, at the moment of use. */
1009
+ export declare function freshUntil(at: string): string | null;
package/dist/scope.js CHANGED
@@ -48,6 +48,7 @@ import { z } from "zod";
48
48
  import {} from "./contracts/acceptance-terms.js";
49
49
  import { parseContract } from "./contracts/contract.js";
50
50
  import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
51
+ import { taskArchived } from "./task-archive.js";
51
52
  import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
52
53
  import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
53
54
  /** Same shape as a runner's credential, for the same reasons. */
@@ -1074,6 +1075,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
1074
1075
  const scope = store.getScope(taskId);
1075
1076
  if (scope === null)
1076
1077
  return { ok: false, reason: "no-scope" };
1078
+ // A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
1079
+ if (taskArchived(store, taskId))
1080
+ return { ok: false, reason: "archived" };
1077
1081
  // The digest is required, not optional. An operator reads a scope, somebody
1078
1082
  // rewrites it, and an approval that did not name what it saw would agree to
1079
1083
  // the new one in silence — which is the whole failure this guards.
@@ -1472,6 +1476,9 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
1472
1476
  return { applies: false };
1473
1477
  const { authority: a, carry } = found;
1474
1478
  const refuse = (why) => ({ applies: true, ok: false, why, authority: a });
1479
+ // A put-away task (v145, task-tidy.md), or a put-away anchor, deploys, merges and carries nothing: restore it first.
1480
+ if (taskArchived(store, taskId) || taskArchived(store, a.anchorTask))
1481
+ return refuse("the release task is put away");
1475
1482
  const newest = newestReleaseGeneration(store, a.repo, a.version, a.pr);
1476
1483
  if (newest === null || newest.id !== a.id)
1477
1484
  return refuse("a newer approval of this release replaced this one");
@@ -1603,3 +1610,9 @@ export function releaseApprovalOf(store, taskId, _now = new Date()) {
1603
1610
  const auto = autoReleaseApprovalOf(store, taskId, "auto-green");
1604
1611
  return auto.ok ? { ok: true, by: auto.by, at: auto.at, method: "auto-green" } : auto;
1605
1612
  }
1613
+ /** When a password typed at `at` (ISO) stops standing in, for a page's words (ISO); null when `at` isn't a time. Words
1614
+ * only: freshnessFor decides whether it stands in, at the moment of use. */
1615
+ export function freshUntil(at) {
1616
+ const ms = Date.parse(at);
1617
+ return Number.isFinite(ms) ? new Date(ms + FRESH_WINDOW_MS).toISOString() : null;
1618
+ }
package/dist/serve.d.ts CHANGED
@@ -7,6 +7,5 @@ export { TASK_COMPOSER_MODES, type TaskComposerMode } from "./server/render-chat
7
7
  export { inboxFingerprints, parseInboxTab, queueScript, type InboxTab } from "./server/render-pages.js";
8
8
  export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
9
9
  export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts } from "./server/render-results.js";
10
- export { settingsGroups, settingsTiles } from "./server/render-settings.js";
11
10
  export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords, type RouteView } from "./server/render-tasks.js";
12
11
  export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, type DecisionServer } from "./server/session.js";
package/dist/serve.js CHANGED
@@ -157,9 +157,11 @@ import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP,
157
157
  import { BODY_CAP, form, loginHref, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
158
158
  import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, NOT_STARTED_SESSION, TASK_COMPOSER_MODES, teamProposalCardParts } from "./server/render-chat.js";
159
159
  import {} from "./server/render-pages.js";
160
- import { ssoStepUps, wrongHostPage } from "./server/render-people.js";
160
+ import { serverErrorPage, ssoStepUps, wrongHostPage } from "./server/render-people.js";
161
161
  import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, sessionWithinLifetime, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
162
162
  import { createTasksHandlers } from './server/tasks.js';
163
+ import { KIT_BEHAVIOR_MARKUP, KIT_STANDALONE_SCRIPT } from './server/kit-standalone.js';
164
+ import { createWelcomeHandlers } from './server/welcome.js';
163
165
  import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
164
166
  import { sourceKey } from "./source-key.js";
165
167
  import { readSsoSettings } from "./sso-settings.js";
@@ -670,14 +672,26 @@ export function createDecisionServer(options) {
670
672
  // A GET or HEAD only reads (docs/plans/get-side-effects.md): store reads that would tidy up as they go don't.
671
673
  const answer = () => handle(request, response);
672
674
  void withPasswordSource(joinSourceOf(request), () => request.method === "GET" || request.method === "HEAD" ? asReadOnlyRequest(answer) : answer()).catch(error => {
673
- // Every unhandled error is logged (v99): the path and the message, never the request's body or query.
674
- logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error) });
675
+ // Every unhandled error is logged (v99): the path and the message, never the request's body or query. The reference
676
+ // is what a person's error page shows, so the log line can be found from it.
677
+ const reference = `err_${randomBytes(4).toString("hex")}`;
678
+ logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error), reference });
675
679
  if (envValue(process.env, "SERVE_DEBUG") === "1")
676
680
  console.error("SERVE ERROR:", error);
677
681
  if (!response.headersSent) {
678
682
  const updating = error instanceof Error && error.message.includes(UPDATE_PAUSED);
679
683
  if (updating)
680
684
  response.setHeader("Retry-After", "5");
685
+ // A browser asking for a page gets a page (Wave E): what happened, whether anything changed, the reference. It
686
+ // holds no error text. Everything else keeps the plain answer.
687
+ if (/\btext\/html\b/.test(String(request.headers.accept ?? "")) && (request.method === "GET" || request.method === "POST")) {
688
+ const read = request.method === "GET";
689
+ const at = new URL(request.url ?? "/", "http://placeholder");
690
+ try {
691
+ return page(response, updating ? 503 : 500, serverErrorPage({ updating, read, reference, at: new Date(), retry: read ? at.pathname + at.search : null }));
692
+ }
693
+ catch { /* the plain answer below */ }
694
+ }
681
695
  respond(response, updating ? 503 : 500, "text/plain; charset=utf-8", updating ? UPDATE_PAUSED : "something broke");
682
696
  }
683
697
  else {
@@ -1058,6 +1072,19 @@ export function createDecisionServer(options) {
1058
1072
  }
1059
1073
  project = wanted;
1060
1074
  }
1075
+ // Home (UI revamp wave B) shows every project unless its address names one (proved the same way), and the rail's
1076
+ // and the dock's Needs-you count follow that same one scope.
1077
+ if (route.id === 'home') {
1078
+ const wanted = url.searchParams.get('project');
1079
+ if (wanted !== null && (url.searchParams.getAll('project').length !== 1 || !visible(wanted) ||
1080
+ ![...managedRepos(), ...store.listProjects().map(one => one.path)].includes(wanted))) {
1081
+ return refuse(response, who, 404, 'That project is not available in this workspace.', '/');
1082
+ }
1083
+ project = wanted;
1084
+ }
1085
+ // All projects, asked for by name on Tasks (its project filter): the rolled-up list, admission-bound as ever.
1086
+ if (route.id === 'work' && url.searchParams.get('all') === '1' && !url.searchParams.has('project') && url.searchParams.getAll('all').length === 1)
1087
+ project = null;
1061
1088
  // Every page's Tasks count covers this one project view, whatever project the page itself shows.
1062
1089
  const facts = requestContext.getStore();
1063
1090
  if (facts !== undefined)
@@ -1618,8 +1645,22 @@ export function createDecisionServer(options) {
1618
1645
  catchUpHtml: extras.catchUpHtml === undefined ? '' : htmlString(extras.catchUpHtml), controlsHtml: extras.controlsHtml === undefined ? '' : htmlString(extras.controlsHtml), notices, view: extras.view ?? null,
1619
1646
  ...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
1620
1647
  pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml === null ? null : htmlString(extras.pageHtml),
1621
- navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
1648
+ navigation: browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel),
1649
+ // The one Needs-you count (tasks only), with the scope it covers: the Tasks list's own count for this project
1650
+ // view (or every project), so the rail, the phone header and Home all read the same number.
1651
+ needsYou: { count: needsYou, saturated: s.chrome.inboxSaturated === true, scope: tasksProject ?? null,
1652
+ label: s.chrome.inboxLabel ?? `${needsYou} ${needsYou === 1 ? 'needs' : 'need'} you`,
1653
+ // Every project, said by name (all=1), so a session's own open project never narrows the list the count opens.
1654
+ href: tasksProject === null ? '/work?all=1&view=needs-you' : `/work?project=${encodeURIComponent(tasksProject)}&view=needs-you` },
1622
1655
  chats,
1656
+ // The same count per project (the rail's project rows), read from the same work index totals.
1657
+ ...(() => { if (sensitive)
1658
+ return {}; try {
1659
+ return { needsYouByProject: projectCounts(clock()).map(one => ({ path: one.repo, count: one.totals['needs-you'] })) };
1660
+ }
1661
+ catch {
1662
+ return {};
1663
+ } })(),
1623
1664
  ...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
1624
1665
  ...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
1625
1666
  ...(s.chrome.update === undefined ? {} : { update: s.chrome.update }),
@@ -1662,7 +1703,10 @@ export function createDecisionServer(options) {
1662
1703
  const functional = s.functional?.script ?? "";
1663
1704
  // Sensitive pages strip the palette and keys but keep the sidebar toggle.
1664
1705
  const sensitiveChrome = sensitive && s.chrome !== undefined ? sidebarScript() : "";
1665
- const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
1706
+ // Kit markup the workspace bundle would have wired (a composer, an overlay, a switch, an idea) gets the kit's own small
1707
+ // behaviour here (kit-standalone.ts); never on a sensitive page, which carries no chrome additions.
1708
+ const kitBehavior = chromeLayer && KIT_BEHAVIOR_MARKUP.test(htmlString(s.body)) ? KIT_STANDALONE_SCRIPT : "";
1709
+ const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT + kitBehavior : sensitiveChrome);
1666
1710
  const nonce = script === "" ? undefined : randomBytes(16).toString("base64");
1667
1711
  const body = chromeLayer
1668
1712
  ? html `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
@@ -1774,7 +1818,7 @@ export function createDecisionServer(options) {
1774
1818
  const update = updateFacts();
1775
1819
  if (update === null || !update.newer || update.release === null || facts.updateSeen === update.release.version)
1776
1820
  return {};
1777
- return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
1821
+ return { update: { version: update.release.version, security: update.release.security, href: "/settings/system#updates", dismissHref: "/settings/updates/dismiss" } };
1778
1822
  })(),
1779
1823
  ...(() => {
1780
1824
  const databaseFile = store.databaseFile();
@@ -2301,6 +2345,7 @@ export function createDecisionServer(options) {
2301
2345
  ...tasksHandlers.registrations, ...flowsHandlers.registrations, ...chatHandlers.registrations,
2302
2346
  ...settingsHandlers.registrations, ...peopleHandlers.registrations, ...pagesHandlers.registrations,
2303
2347
  ...remoteHandlers.registrations, ...liveHandlers.registrations, ...terminalHandlers.registrations, ...createSweepHandlers(handlerRuntime).registrations,
2348
+ ...createWelcomeHandlers(handlerRuntime).registrations,
2304
2349
  ]);
2305
2350
  /** The row's role, after admission and before its one handler. */
2306
2351
  function dispatchConsole(route, ctx) {
@@ -2333,7 +2378,6 @@ export { TASK_COMPOSER_MODES } from "./server/render-chat.js";
2333
2378
  export { inboxFingerprints, parseInboxTab, queueScript } from "./server/render-pages.js";
2334
2379
  export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
2335
2380
  export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches } from "./server/render-results.js";
2336
- export { settingsGroups, settingsTiles } from "./server/render-settings.js";
2337
2381
  export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
2338
2382
  export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
2339
2383
  /**
@@ -0,0 +1,9 @@
1
+ import type { BrowserAgentPhase, BrowserAgentsSystem, BrowserComputers } from "../browser-workspace.js";
2
+ import type { Store, WorktreeRow } from "../store.js";
3
+ /** Every registered computer that isn't retired, plus a retired one still holding reserved work, as Agents lists it. */
4
+ export declare function computersOf(store: Store, now: Date, visible: (repo: string | null) => boolean, manage: boolean): BrowserComputers;
5
+ /** Which provider and model runs each phase for the open project (or the installation), and where that choice comes from. */
6
+ export declare function phasesOf(store: Store, project: string | null): BrowserAgentPhase[];
7
+ /** The rest of what /system showed: its workspaces, the background service on the open project, notifications waiting
8
+ * to go out and external work. */
9
+ export declare function systemOf(store: Store, project: string | null, worktreeShown: (tree: WorktreeRow) => boolean, visible: (repo: string | null) => boolean): BrowserAgentsSystem;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The Agents page's installation parts (UI revamp wave B): the computers that build (what /fleet showed), which model
3
+ * runs each phase and the rest of what /system showed. Saved facts only, filtered through the caller's `visible`, for
4
+ * a person with access to the whole installation; nothing here grants an act (register, retire, move and note keep
5
+ * their own handlers and checks).
6
+ */
7
+ import { INSTALLATION_SCOPE, resolvePhaseAgent } from "../agentconfig.js";
8
+ import { agentModelName } from "../model-names.js";
9
+ import { projectName } from "../project.js";
10
+ import { isAlive } from "../runner.js";
11
+ import { taskHref } from "./http.js";
12
+ /** Every registered computer that isn't retired, plus a retired one still holding reserved work, as Agents lists it. */
13
+ export function computersOf(store, now, visible, manage) {
14
+ const queued = store.fleetQueue(now);
15
+ const claims = store.liveClaims(null, now);
16
+ const owned = new Set(queued.map(one => one.assignedRunner).filter((one) => one !== null));
17
+ const runners = store.listRunners().filter(one => one.retiredAt === null || owned.has(one.name));
18
+ const list = runners.map(runner => {
19
+ const live = claims.filter(claim => claim.runner === runner.name);
20
+ const shown = live.filter(claim => visible(claim.repo ?? null));
21
+ const alive = isAlive(runner, now);
22
+ return {
23
+ name: runner.name, host: runner.host, capacity: runner.capacity, retired: runner.retiredAt !== null,
24
+ state: !alive ? "offline" : live.length > 0 ? "building" : "idle",
25
+ building: shown.map(claim => ({ taskId: claim.taskId, title: store.getTask(claim.taskId)?.title ?? claim.taskId, href: taskHref(claim.taskId),
26
+ project: claim.repo == null ? null : projectName(claim.repo) })),
27
+ hidden: live.length - shown.length,
28
+ heardAt: runner.heartbeatAt, registeredAt: runner.registeredAt,
29
+ projects: runner.repos.filter(repo => visible(repo)).map(repo => projectName(repo)),
30
+ note: runner.queueNote ?? null,
31
+ reserved: queued.filter(one => one.assignedRunner === runner.name && visible(one.repo))
32
+ .map(one => ({ id: one.id, title: one.title, href: taskHref(one.id), project: one.repo === null ? null : projectName(one.repo), taken: one.taken })),
33
+ };
34
+ });
35
+ return { list, manage, queueRevision: store.queueRevision(), shared: queued.filter(one => one.assignedRunner === null && visible(one.repo)).length };
36
+ }
37
+ const PHASES = ["plan", "build", "repair", "review"];
38
+ /** Which provider and model runs each phase for the open project (or the installation), and where that choice comes from. */
39
+ export function phasesOf(store, project) {
40
+ return PHASES.map(phase => {
41
+ const answer = resolvePhaseAgent(store, phase, project, {});
42
+ const row = (project === null ? null : store.phaseConfig(project, phase)) ?? store.phaseConfig(INSTALLATION_SCOPE, phase);
43
+ const by = row?.updatedBy == null ? "" : ` by ${row.updatedBy}`;
44
+ if (!answer.ok)
45
+ return { phase, model: null, problem: answer.problem, source: "", noDollars: false };
46
+ const source = answer.source === "project" ? `Chosen for this project${by}`
47
+ : answer.source === "installation" ? `Set for the whole installation${by}` : "The default: nothing chosen";
48
+ return { phase, model: agentModelName(answer.spec.provider, answer.spec.model ?? null), problem: null, source, noDollars: answer.spec.provider !== "claude" };
49
+ });
50
+ }
51
+ /** The rest of what /system showed: its workspaces, the background service on the open project, notifications waiting
52
+ * to go out and external work. */
53
+ export function systemOf(store, project, worktreeShown, visible) {
54
+ const workspaces = store.listWorktrees().filter(worktreeShown).map(tree => {
55
+ const leased = tree.leasedAt !== null && tree.releasedAt === null;
56
+ return { name: tree.path.split("/").pop() ?? tree.path, branch: tree.branch, state: leased ? "building" : tree.verified ? "free" : "needs review" };
57
+ });
58
+ const episode = project === null ? null : store.latestWatchEpisode(project);
59
+ const service = episode === null ? null : { running: episode.endedAt === null, at: episode.endedAt ?? episode.startedAt, built: episode.built, broke: episode.broke };
60
+ const external = store.listGrants()
61
+ .filter(one => one.dispatch === true && one.remoteRepo != null && visible(one.repo))
62
+ .map(one => {
63
+ const open = store.listNotifications("all").find(n => n.kind === "sync-failed" && n.resolvedAt === null && n.dedupeKey.startsWith(`sync:${one.remoteRepo}:`))?.subject ?? null;
64
+ return { repo: String(one.remoteRepo), state: one.dispatchBlocked != null ? "Dispatch blocked: the tracker connection needs repair (toolroll sync says why)" : open !== null ? `Sync failing: ${open}` : "Syncing normally" };
65
+ });
66
+ return { workspaces, service, outbox: store.pendingForAttention().length, external };
67
+ }