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
@@ -96,16 +96,17 @@ export function browserProjectsOf(projects) {
96
96
  }
97
97
  export function browserNavigationOf(path, project = null, needsYou = 0, tasksProject = project, needsYouLabel) {
98
98
  const pathname = path.split('?')[0].split('#')[0];
99
- const knowledge = pathname === '/settings/knowledge' || pathname.startsWith('/settings/knowledge/');
99
+ const under = (...roots) => roots.some(root => pathname === root || pathname.startsWith(`${root}/`));
100
+ // The rail (UI revamp, wave B): Home carries the one Needs-you count; the lead chat and an agent's question live
101
+ // under Home, a task's pages under Tasks, every computer and terminal under Agents, every setting under Settings.
100
102
  return [
101
- { label: 'Chat', href: '/chat', active: pathname === '/chat' },
102
- { label: 'Tasks', href: `/work${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
103
- active: pathname === '/work' || pathname === '/tasks' || pathname.startsWith('/t/') || pathname.startsWith('/r/'),
103
+ { label: 'Home', href: '/', active: pathname === '/' || pathname === '/chat' || under('/d', '/activity', '/morning'),
104
104
  ...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
105
- { label: 'Agents', href: '/agents', active: pathname === '/agents' || pathname === '/peek' },
106
- { label: 'Flows', href: '/flows', active: pathname === '/flows' || pathname.startsWith('/flows/') },
107
- { label: 'Projects', href: '/projects', active: pathname === '/projects' },
108
- { label: 'Knowledge', href: `/settings/knowledge${project === null ? '' : `?repo=${encodeURIComponent(project)}`}`, active: knowledge },
109
- { label: 'Settings', href: '/settings', active: !knowledge && (pathname === '/settings' || pathname.startsWith('/settings/')) },
105
+ { label: 'Tasks', href: `/work${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
106
+ active: under('/work', '/tasks', '/t', '/r', '/done', '/board', '/queue', '/inbox', '/review') },
107
+ { label: 'Flows', href: '/flows', active: under('/flows', '/recipes', '/kits', '/routines') },
108
+ { label: 'Agents', href: '/agents', active: under('/agents', '/peek', '/term', '/fleet', '/system', '/runs') },
109
+ { label: 'Projects', href: '/projects', active: under('/projects') },
110
+ { label: 'Settings', href: '/settings', active: under('/settings', '/spend', '/mode', '/people', '/ledger', '/menu') },
110
111
  ];
111
112
  }
@@ -153,6 +153,16 @@ export declare const CHAT_ACTIONS: {
153
153
  readonly protected: false;
154
154
  readonly password: false;
155
155
  };
156
+ readonly flow_browser: {
157
+ readonly label: "Allow browser";
158
+ readonly protected: true;
159
+ readonly password: true;
160
+ };
161
+ readonly flow_browser_off: {
162
+ readonly label: "Turn off browser";
163
+ readonly protected: false;
164
+ readonly password: false;
165
+ };
156
166
  readonly subagent_create: {
157
167
  readonly label: "Add subagent";
158
168
  readonly protected: false;
@@ -266,7 +276,7 @@ export declare function sharedActionReviewPath(id: number): string;
266
276
  export declare function mintSharedActionReview(store: Store, who: VerifiedApprover, id: number, root: string, now: Date): {
267
277
  payload: {
268
278
  version: 1;
269
- operation: "decision_record" | "decision_retire" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove";
279
+ operation: "decision_record" | "decision_retire" | "flow_browser" | "flow_browser_off" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove";
270
280
  request: {
271
281
  [x: string]: unknown;
272
282
  };
@@ -25,6 +25,7 @@ import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
25
25
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
26
26
  import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
27
27
  import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
28
+ import { browserViewOf, liveBrowserGrant, revokeFlowBrowser, signFlowBrowser } from "./flow-browser.js";
28
29
  import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
29
30
  import { describeSchedule, parseSchedule } from "./flow-schedule.js";
30
31
  import { dirname } from "node:path";
@@ -109,6 +110,10 @@ export const CHAT_ACTIONS = {
109
110
  flow_trigger_pause: { label: "Pause trigger", protected: false, password: false },
110
111
  flow_trigger_resume: { label: "Turn trigger on", protected: false, password: false },
111
112
  flow_trigger_remove: { label: "Remove trigger", protected: false, password: false },
113
+ // Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their
114
+ // own password on the secure screen, never on a chat card or a paired phone's tap. Turning it off lowers authority.
115
+ flow_browser: { label: "Allow browser", protected: true, password: true },
116
+ flow_browser_off: { label: "Turn off browser", protected: false, password: false },
112
117
  subagent_create: { label: "Add subagent", protected: false, password: false },
113
118
  subagent_soul: { label: "Update soul file", protected: false, password: false },
114
119
  subagent_state: { label: "Change subagent", protected: false, password: false },
@@ -710,6 +715,32 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
710
715
  terms.push("Only what happens from now on counts; nothing already there is added.");
711
716
  terms.push("Work a card starts still waits for your usual approvals.");
712
717
  }
718
+ else if (operation === "flow_browser" || operation === "flow_browser_off") {
719
+ const { flow } = flowTarget;
720
+ const view = browserViewOf(store, flow);
721
+ if (view === null)
722
+ throw Error(`No zone in ${flow.name} says it starts a browser. Mark its script zone first (edit the flow), then propose this.`);
723
+ // As on the flow page (route table: instance access), a project-limited account can't allow browser use.
724
+ if (operation === "flow_browser" && store.accountOf(who.name)?.projects !== null)
725
+ throw Error("Allowing browser use needs an account with access to every project.");
726
+ if (operation === "flow_browser_off") {
727
+ if (liveBrowserGrant(store, flow.id) === null)
728
+ throw Error(`Browser use isn't allowed for ${flow.name}.`);
729
+ state = { flow: flow.id, grant: liveBrowserGrant(store, flow.id).id };
730
+ title = `Turn off browser use in ${flow.name}`;
731
+ terms.push(`${flow.name}'s browser zones stop and wait until a person allows browser use again.`);
732
+ }
733
+ else {
734
+ if (view.status === "allowed")
735
+ throw Error(`${view.words}.`);
736
+ if (view.digest === null)
737
+ throw Error(view.problem ?? "This flow's browser zones can't be approved as they are.");
738
+ // The approval the person reads and the one their password signs: the digest is re-derived when they confirm.
739
+ state = { flow: flow.id, revision: flow.revision, digest: view.digest };
740
+ title = `Allow browser use in ${flow.name}`;
741
+ terms.push(`Flow: ${flow.name} · project ${project}`, ...view.zones.map(zone => `${zone.title} runs ${zone.script} v${zone.version}\nScript digest ${zone.digest}`), "Each run of these zones starts a headless Chromium.", ...view.terms, `Approval digest ${view.digest}`);
742
+ }
743
+ }
713
744
  else if (operation === "flow_trigger_pause" || operation === "flow_trigger_resume" || operation === "flow_trigger_remove") {
714
745
  const { flow, trigger } = flowTarget;
715
746
  const config = trigger === null ? null : triggerConfigOf(trigger);
@@ -1099,6 +1130,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1099
1130
  }
1100
1131
  // The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
1101
1132
  const savedStamp = payload.stamp;
1133
+ let personProved = null;
1102
1134
  payload = live;
1103
1135
  const config = CHAT_ACTIONS[live.operation], req = live.request;
1104
1136
  const challenged = options.via !== "web" &&
@@ -1113,10 +1145,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1113
1145
  // A scope approval proves its person inside approve() (one transaction with the seal, under the same freshness
1114
1146
  // rule as the task page); here an empty field is only looked at, never spent (an explicit re-check is one-use).
1115
1147
  const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
1148
+ const proved = config.password && !emptyApproval ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
1149
+ // How the person was proved, for an approval that records it (browser use: flow-browser.ts refuses any but a person's).
1150
+ if (proved?.ok === true)
1151
+ personProved = proved.method;
1116
1152
  if (config.password &&
1117
1153
  (emptyApproval
1118
1154
  ? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
1119
- : !authenticateApprover(store, who.name, options.review.password, payload.repo).ok))
1155
+ : proved?.ok !== true))
1120
1156
  return refuse("needs-confirm", "Confirm with your password in the secure screen.");
1121
1157
  }
1122
1158
  try {
@@ -1156,6 +1192,22 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1156
1192
  ? {}
1157
1193
  : { restore: Number(req["restore"]) }),
1158
1194
  }, now);
1195
+ else if (payload.operation === "flow_browser") {
1196
+ // Only through the secure screen with the person's own password (protected + password above); signFlowBrowser
1197
+ // re-reads the flow, its zones and scripts in this transaction and refuses unless they are what the card showed.
1198
+ const flow = Number(payload.state["flow"]);
1199
+ // signFlowBrowser re-reads the account's generation and reach in its transaction (it may have narrowed since).
1200
+ const signed = signFlowBrowser(store, { flow, signer: actor, proof: personProved, sawDigest: String(payload.state["digest"] ?? ""), console: { generation: who.generation } }, now);
1201
+ if (!signed.ok)
1202
+ throw Error(signed.message);
1203
+ return { ok: true, taskId: null, said: "Browser allowed. Its browser zones can start Chromium now.", href: `/flows/${flow}` };
1204
+ }
1205
+ else if (payload.operation === "flow_browser_off") {
1206
+ const flow = Number(payload.state["flow"]);
1207
+ if (!revokeFlowBrowser(store, flow, actor, now))
1208
+ throw Error("Browser use was already off.");
1209
+ return { ok: true, taskId: null, said: "Browser turned off. Its browser zones wait until someone allows it again.", href: `/flows/${flow}` };
1210
+ }
1159
1211
  else if (payload.operation.startsWith("flow_")) {
1160
1212
  const done = runFlowAction(store, payload, actor, who.repos, options.root, now);
1161
1213
  return { ok: true, taskId: null, said: done.said, href: done.href };
@@ -578,6 +578,7 @@ export const CHAT_ACTION_PARITY = {
578
578
  ask_owner: { support: "direct", how: "One question with 2-4 options and Something else, drawn as buttons under the reply; the tapped option is sent as the person's next message.", gap: null },
579
579
  search_project_memory: { support: "direct", how: "Read during a turn: one search over decisions, references, lessons and the conversations the person may read.", gap: null },
580
580
  list_tasks: { support: "direct", how: "Read during a turn.", gap: null },
581
+ tidy_preview: { support: "direct", how: "Read during a turn: what Put away old tasks would put away now and why. Only a person puts tasks away, on the console or with the CLI command it names.", gap: null },
581
582
  get_task: {
582
583
  support: "direct",
583
584
  how: "Read during a turn; a reply to a result message pins the exact execution.",
@@ -76,7 +76,7 @@ export declare const CHAT_CONTROLS: {
76
76
  };
77
77
  readonly providers: {
78
78
  readonly label: "Connect an agent";
79
- readonly href: "/settings#providers";
79
+ readonly href: "/settings/agents#providers";
80
80
  };
81
81
  readonly skills: {
82
82
  readonly label: "Manage skills";
@@ -21,7 +21,7 @@ export const CHAT_CONTROLS = {
21
21
  integrations: { label: "Set up integrations", href: "/settings/integrations" },
22
22
  settings: { label: "Open settings", href: "/settings" },
23
23
  permissions: { label: "Review permissions", href: "/settings" },
24
- providers: { label: "Connect an agent", href: "/settings#providers" },
24
+ providers: { label: "Connect an agent", href: "/settings/agents#providers" },
25
25
  skills: { label: "Manage skills", href: "/settings/skills" },
26
26
  tools: { label: "Manage tools", href: "/settings/tools" },
27
27
  knowledge: { label: "Edit project knowledge", href: "/settings/knowledge" },
@@ -10,7 +10,7 @@ import { telegramProgressCard } from "./telegram-progress.js";
10
10
  import { assignmentStatusFacts, headlineEmoji, taskStatusOf } from "./task-status.js";
11
11
  import { asksToFinish, batchLine, chatText, chatTitle, factLinkLabel, mentions, READY_ACTIONS } from "./chat-voice.js";
12
12
  import { phoneText, projectLabel } from "./telegram-status.js";
13
- import { isLifecycleNotification } from "./store.js";
13
+ import { isLifecycleNotification, taskArchived } from "./store.js";
14
14
  import { leadSubjectOf } from "./lead-voice.js";
15
15
  import { decideFallbackLink, decideTargetOf } from "./chat-decide.js";
16
16
  import { YOUIYOUX_CARD_KIND } from "./contracts/youiyoux-request.js";
@@ -199,7 +199,8 @@ export function eveningDigestText(store, account, now, root) {
199
199
  const visible = (row) => row.project !== null && store.accountCanAccess(account, row.project);
200
200
  const title = (taskId) => chatTitle(store, taskId);
201
201
  const finished = new Map(), failed = new Map(), waits = new Map();
202
- const facts = store.taskFactsSince(recent).filter(visible);
202
+ // A put-away task (v145) is out of the digest too: nothing about it waits, finished or failed for anyone now.
203
+ const facts = store.taskFactsSince(recent).filter(visible).filter(row => row.taskId === null || !taskArchived(store, row.taskId));
203
204
  for (const row of facts) {
204
205
  if (row.createdAt < since || row.taskId === null)
205
206
  continue;
@@ -7,17 +7,37 @@ export type ChatSettingsOptions = {
7
7
  now?: Date;
8
8
  who?: string;
9
9
  };
10
- /** What differs between the Slack and Discord settings pages; everything else is one page. */
10
+ /** What differs between the Slack, Discord and Teams settings pages; everything else is one page. */
11
11
  export type ChatSettingsPage = {
12
- provider: "slack" | "discord";
12
+ provider: "slack" | "discord" | "teams";
13
13
  label: string;
14
14
  credentials: ChatIdentity | null;
15
- /** How to create the app, shown before it is connected. */
16
- setup: Html;
17
- /** The connect form's fields around the password. */
18
- connect: (password: Html) => Html;
19
- /** The "Access and setup" paragraphs. */
20
- access: Html;
15
+ /** One sentence on what connecting gives, shown before it is connected. */
16
+ intro: string;
17
+ /** How to create the app, as list items, shown before it is connected. */
18
+ steps: Html;
19
+ /** The connect form's own fields (the password is the kit's). */
20
+ fields: Html;
21
+ /** One line under the connect fields. */
22
+ fieldsNote: string;
23
+ /** Where the pairing message is sent ("in a direct message to your Toolroll Slack app"). */
24
+ pairWhere: string;
25
+ /** What to try once paired. */
26
+ tryLine: Html;
27
+ /** Lines shown with the connection (the Teams messaging endpoint). */
28
+ connection?: Html;
29
+ /** The status words while connected and while waiting. */
30
+ liveWords?: readonly [on: string, off: string];
31
+ /** The "Access and setup" paragraphs, if any. */
32
+ access?: Html;
21
33
  };
22
- /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
34
+ /** A secret the connect form posts (an app or bot token, a client secret): masked, never given a value, kept out of
35
+ * password managers. */
36
+ export declare function chatSecretRow(options: {
37
+ id: string;
38
+ name: string;
39
+ label: string;
40
+ placeholder?: string;
41
+ }): Html;
42
+ /** Settings → Slack, Discord or Teams: connect the app once, then each teammate pairs their own account. */
23
43
  export declare function chatSettingsHtml(store: Store, dir: string, options: ChatSettingsOptions, page: ChatSettingsPage): Html;
@@ -1,41 +1,72 @@
1
+ import { settingsSubPage } from "./settings-groups-ui.js";
1
2
  import { loadPrimary } from "./webhooks.js";
2
- import { html, postForm } from "./html.js";
3
+ import { html, joinHtml } from "./html.js";
3
4
  import { ChatState } from "./chat-delivery-state.js";
4
- /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
5
+ import { acts, btn, chip, codeLine, disclose, formRow, infoLine, input, note, notice, settingsSection } from "./server/kit.js";
6
+ import { kitSectionForm, SECRET_OPT_OUTS } from "./settings-kit.js";
7
+ /** A secret the connect form posts (an app or bot token, a client secret): masked, never given a value, kept out of
8
+ * password managers. */
9
+ export function chatSecretRow(options) {
10
+ return formRow(options.label, null, input({ id: options.id, name: options.name, type: "password",
11
+ attrs: { autocomplete: "off", spellcheck: "false", "data-so-secret": true, ...SECRET_OPT_OUTS, required: true, placeholder: options.placeholder, "aria-label": options.label } }));
12
+ }
13
+ /** Settings → Slack, Discord or Teams: connect the app once, then each teammate pairs their own account. */
5
14
  export function chatSettingsHtml(store, dir, options, page) {
6
15
  const { provider, label, credentials } = page, state = new ChatState(store, provider), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
7
16
  const runtime = credentials ? state.runtime(credentials.installation) : null;
8
- const now = options.now ?? new Date(), live = runtime &&
17
+ const now = options.now ?? new Date(), live = runtime !== null &&
9
18
  runtime.leaseUntil !== null &&
10
19
  runtime.leaseUntil > now.toISOString() &&
11
20
  runtime.connected;
12
- const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
13
- const post = (action, content) => postForm(`/settings/${provider}/${action}`, content, { attrs: { class: "card" } });
14
- const parts = [];
21
+ const route = (action) => `/settings/${provider}/${action}`;
22
+ const problem = options.problem ? notice(options.problem) : "";
23
+ const access = page.access === undefined ? "" : disclose("Access and setup", page.access);
24
+ const sections = [];
15
25
  if (!credentials) {
16
- parts.push(html `<p>Manage projects and review results in a private ${label} conversation.</p>`, page.setup, post("connect", page.connect(password)));
26
+ sections.push(settingsSection({ id: "setup", title: `1. Create the ${label} app`, body: page.steps }), settingsSection({ id: "connect", title: `2. Connect ${label}`,
27
+ body: kitSectionForm({ proof: `${provider}-open`, action: route("connect"), id: "connect-form", save: { label: `Connect ${label}` },
28
+ confirm: `Connect ${label}`, body: joinHtml([page.fields, note(page.fieldsNote)]) }) }), settingsSection({ id: "pair", title: "3. Pair your account", body: note(`Once ${label} is connected, pair your own account here.`) }));
29
+ return settingsSubPage("alerts", label, joinHtml([problem, ...sections, access]), { lead: page.intro });
30
+ }
31
+ const [onWords, offWords] = page.liveWords ?? ["Connected", "Waiting for connection"];
32
+ sections.push(settingsSection({ id: "connection", title: credentials.workspace, state: html ` ${chip(live ? onWords : offWords, live ? "done" : "")}`,
33
+ body: joinHtml([
34
+ page.connection ?? "",
35
+ runtime?.problem ? notice(String(runtime.problem)) : "",
36
+ !live && !runtime?.problem ? note(`Keep Toolroll's builder running to receive ${label} messages.`) : "",
37
+ disclose("Connection settings", kitSectionForm({ proof: `${provider}-close`, action: route("disconnect"), id: "disconnect-form",
38
+ save: { label: `Disconnect ${label}`, variant: "danger" }, confirm: `Disconnect ${label}`, body: html `` })),
39
+ ]) }));
40
+ const teammates = others ? `${others} teammate${others === 1 ? " is" : "s are"}` : "";
41
+ if (options.code) {
42
+ // The code shows once, on this page only: selectable text, never in a link or an address.
43
+ sections.push(settingsSection({ id: "pair", title: "Pair your account", state: " · Code ready",
44
+ lead: `Send this ${page.pairWhere}. It expires in 10 minutes.`,
45
+ body: joinHtml([codeLine(`pair ${options.code}`), note("Once you've sent it, this page says your account is paired."),
46
+ acts(btn("Check connection", { sm: true, href: `/settings/${provider}` }))]) }));
47
+ }
48
+ else if (binding && state.live(binding)) {
49
+ const { pending, dropped: failed } = state.partCounts(binding.id);
50
+ sections.push(settingsSection({ id: "pair", title: "Your account", state: " · Paired",
51
+ lead: `Your ${label} account is paired.${teammates ? ` ${teammates} paired too.` : ""}`,
52
+ body: joinHtml([
53
+ pending ? infoLine(`${pending} ${pending === 1 ? "reply is" : "replies are"} waiting to send.`) : "",
54
+ failed ? notice(`${failed} ${failed === 1 ? "reply" : "replies"} could not be delivered. Open the saved chat to recover them.`) : "",
55
+ note(page.tryLine),
56
+ acts(btn("Open saved chat", { sm: true, href: "/chat" })),
57
+ loadPrimary(process.env, dir) === provider
58
+ ? formRow("Task updates", null, html `<span>Sent here</span>`, "inline")
59
+ : kitSectionForm({ proof: `${provider}-alerts`, action: route("alerts"), id: "alerts-form", save: { label: "Send task updates here", variant: "outline" },
60
+ body: note(`Task updates aren't sent to ${label} now.`) }),
61
+ kitSectionForm({ proof: `${provider}-close`, action: route("unpair"), id: "unpair-form", save: { label: "Unpair my account", variant: "danger" },
62
+ confirm: `Unpair your ${label} account`, body: note("Unpairing ends every open button in your chat. Teammates are unaffected.") }),
63
+ ]) }));
17
64
  }
18
65
  else {
19
- parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
20
- if (runtime?.problem)
21
- parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
22
- if (!live && !runtime?.problem)
23
- parts.push(html `<p>Keep the Toolroll worker running to receive ${label} messages.</p>`);
24
- if (options.code) {
25
- parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll ${label} app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/${provider}">Check connection</a>`);
26
- }
27
- else if (binding && state.live(binding)) {
28
- const { pending, dropped: failed } = state.partCounts(binding.id);
29
- parts.push(html `<p>Your ${label} account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === provider
30
- ? html `<p>Task updates are sent here.</p>`
31
- : post("alerts", html `<button type="submit">Send task updates here</button>`));
32
- }
33
- else {
34
- parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
35
- }
36
- if (binding && state.live(binding) && !options.code)
37
- parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
38
- parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect ${label}</button>`)}</details>`);
66
+ sections.push(settingsSection({ id: "pair", title: "Pair your account", state: " · Not paired",
67
+ lead: `Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${teammates ? ` ${teammates} already paired.` : ""}`,
68
+ body: kitSectionForm({ proof: `${provider}-open`, action: route("pair"), id: "pair-form", native: true, save: { label: "Create pairing code" },
69
+ confirm: `Pair your ${label} account`, body: note("You get a one-time message to send to the app.") }) }));
39
70
  }
40
- return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>${label}</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary>${page.access}</details></section>`;
71
+ return settingsSubPage("alerts", label, joinHtml([problem, ...sections, access]));
41
72
  }
@@ -28,4 +28,5 @@ export type CheckSettingsView = {
28
28
  problem: string | null;
29
29
  };
30
30
  export declare function checkSettingsHtml(view: CheckSettingsView): Html;
31
- export declare const CHECK_SETTINGS_CSS = "\n .check-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .check-settings p, .check-settings h2 { margin: 0; }\n .check-settings h2 { font-size: 1rem; }\n .check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }\n .check-settings legend { font-weight: 600; margin-bottom: .25rem; }\n .check-settings label { display: grid; gap: .25rem; }\n .check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }\n .check-settings .check-choice > span { display: grid; gap: .1rem; }\n .check-settings .check-choice input { margin-top: .2rem; }\n .check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }\n .check-settings button { min-height: 44px; justify-self: start; }\n .check-settings .check-command { overflow-wrap: anywhere; }\n .check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .check-settings button { justify-self: stretch; } }\n .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }\n .follow-ups .follow-up-act { margin: 0; }\n .follow-ups button { min-height: 44px; }\n @media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }\n";
31
+ /** The follow-up list's look (result-follow-ups.ts); the checks page itself is kit markup. */
32
+ export declare const CHECK_SETTINGS_CSS = "\n .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }\n .follow-ups .follow-up-act { margin: 0; }\n .follow-ups button { min-height: 44px; }\n @media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }\n";
@@ -2,72 +2,81 @@
2
2
  * words. One choice (Quick, Full, Off) and the quick command beside the full
3
3
  * one. Every change takes the password and lands in the ledger; approving a
4
4
  * command restates exactly what will run, unattended, before the yes. */
5
- import { html, postForm, reauth } from "./html.js";
5
+ import { html, joinHtml } from "./html.js";
6
6
  import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
7
7
  import { BATCH_HINT } from "./batch-policy.js";
8
+ import { codeLine, disclose, formRow, infoLine, input, note, notice, radios, settingsSection, textarea } from "./server/kit.js";
9
+ import { kitSectionForm } from "./settings-kit.js";
10
+ const list = (items) => html `<ul class="k-note">${items.map(one => html `<li>${one}</li>`)}</ul>`;
8
11
  export function checkSettingsHtml(view) {
9
- const post = (act, body, attrs) => postForm("/settings/checks", body, { attrs, hidden: { repo: view.repo, act } });
10
- const password = html `${reauth(html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
11
- const notes = [
12
- view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
13
- view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
14
- ];
15
- const head = html `<p class="meta">${view.name} · <span class="mono">${view.repo}</span></p>${notes}`;
16
- const command = (one) => html `<p class="mono check-command">${one.command}</p><p class="meta">Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${one.approvedBy}</p>`;
12
+ const form = (id, act, body, save, extra = {}) => kitSectionForm({ proof: "checks", action: "/settings/checks", id, hidden: { repo: view.repo, act, ...extra }, save, body });
13
+ const head = joinHtml([
14
+ view.problem === null ? "" : notice(view.problem),
15
+ view.said === null ? "" : infoLine(view.said),
16
+ note(html `${view.name} · <span class="k-mono">${view.repo}</span>`),
17
+ ]);
18
+ const command = (one) => joinHtml([codeLine(one.command), note(`Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${one.approvedBy}`)]);
17
19
  // Quick with no quick command runs the full check; say so where the choice is made.
18
20
  const fallsBack = view.level === "quick" && view.quick === null;
19
- const current = html `<p><strong>${CHECK_LEVEL_WORDS[view.level]}</strong> · ${CHECK_LEVEL_HINTS[view.level]}</p>${fallsBack ? html `<p class="meta">No quick command yet, so builds run the full check.</p>` : ""}${view.level !== "full" && view.full !== null ? html `<p class="meta">The full check still runs when a pull request opens, and Merge waits for it.</p>` : ""}`;
20
- if (!view.canChange) {
21
- return html `${head}<section class="card check-settings">${current}</section>${batchHtml(view, null)}${reviewHtml(view, null)}<section class="card check-settings"><h2>Quick check</h2>${view.quick === null ? html `<p class="meta">None yet.</p>` : command(view.quick)}</section><section class="card check-settings"><h2>Full check</h2>${view.full === null ? html `<p class="meta">None yet.</p>` : command(view.full)}</section>`;
21
+ const levelNotes = joinHtml([
22
+ fallsBack ? note("No quick command yet, so builds run the full check.") : "",
23
+ view.level !== "full" && view.full !== null ? note("The full check still runs when a pull request opens, and Merge waits for it.") : "",
24
+ ]);
25
+ const level = settingsSection({ id: "check-level", title: "After each build", state: ` · ${CHECK_LEVEL_WORDS[view.level]}`, lead: CHECK_LEVEL_HINTS[view.level],
26
+ body: !view.canChange ? levelNotes : joinHtml([levelNotes, kitSectionForm({ proof: "checks", action: "/settings/checks", id: "check-level-form", gate: true,
27
+ hidden: { repo: view.repo, act: "level" }, confirm: "Change checks",
28
+ body: joinHtml([
29
+ formRow("After each build", "A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.", radios({ name: "level", legend: "After each build", current: view.level, items: CHECK_LEVELS.map(one => ({ value: one, label: CHECK_LEVEL_WORDS[one], text: CHECK_LEVEL_HINTS[one] })) }), "wide"),
30
+ ]) })]) });
31
+ const commandForm = (id, value, submit) => form(id, "quick", joinHtml([
32
+ formRow("Command", null, textarea({ id: `${id}-command`, name: "command", value, rows: 2, attrs: { maxlength: 2000, spellcheck: "false", required: true, "aria-label": "Command" } }), "wide"),
33
+ formRow("Time limit", "Seconds, up to 3600.", input({ id: `${id}-timeout`, name: "timeout", type: "number", value: String(view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)),
34
+ attrs: { min: 1, max: 3600, required: true, "aria-label": "Time limit in seconds" } }), "inline"),
35
+ list(["Runs unattended right after every Quick build, in that build's checkout.", "Plain environment: no credentials.", "A failure shows on the result; nothing is blocked."]),
36
+ ]), { label: submit, variant: "outline" });
37
+ const quick = settingsSection({ id: "quick-check", title: "Quick check", state: view.quick === null ? " · None yet" : "",
38
+ lead: view.quick === null && view.canChange
39
+ ? view.suggestion === null ? "Typecheck and the tests near the change, ideally under 2 minutes." : "Suggested from the project's scripts. Edit it before approving."
40
+ : undefined,
41
+ body: view.quick === null
42
+ ? view.canChange ? commandForm("quick-form", view.suggestion ?? "", "Approve quick check") : ""
43
+ : joinHtml([command(view.quick), !view.canChange ? "" : joinHtml([
44
+ disclose("Change quick check", commandForm("quick-change-form", view.quick.command, "Approve change")),
45
+ disclose("Remove quick check", form("quick-clear-form", "quick-clear", note("Quick builds run the full check instead."), { label: "Remove", variant: "danger" })),
46
+ ])]) });
47
+ const full = settingsSection({ id: "full-check", title: "Full check", state: view.full === null ? " · None yet" : "",
48
+ body: view.full === null
49
+ ? joinHtml([note("Approve one from the command line:"), codeLine(`toolroll verify set --repo … --command "…"`)])
50
+ : command(view.full) });
51
+ return joinHtml([head, level, batchHtml(view), reviewHtml(view), quick, full]);
52
+ /** Batch checks: one state and one sentence; turning it on restates exactly what happens before the password. */
53
+ function batchHtml(view) {
54
+ const batch = view.batch;
55
+ if (batch === undefined)
56
+ return "";
57
+ const act = batch.on ? "Turn off" : "Turn on";
58
+ const terms = batch.on ? note("Each result runs its own full check again. Results already waiting are checked now.")
59
+ : list(["When results finish within 10 minutes of each other, they're merged in a temporary checkout and the full check runs once.",
60
+ "If it fails, the batch is split until the result that breaks it is found. Results that conflict are checked on their own.",
61
+ "Nothing is merged into a real branch. Each result still lands on its own."]);
62
+ return settingsSection({ id: "batch-checks", title: "Batch checks", state: html `<span data-batch-checks="${batch.on ? "on" : "off"}"> · ${batch.on ? "On" : "Off"}</span>`, lead: BATCH_HINT,
63
+ body: joinHtml([batch.on && view.level !== "full" ? note("Only Full checks are batched.") : "",
64
+ view.canChange ? disclose(act, form("batch-form", "batch", terms, { label: act, variant: "outline" }, { on: batch.on ? "0" : "1" })) : ""]) });
65
+ }
66
+ /** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
67
+ function reviewHtml(view) {
68
+ const review = view.review;
69
+ if (review === undefined)
70
+ return "";
71
+ const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
72
+ const act = review.on ? "Turn off" : "Turn on";
73
+ return settingsSection({ id: "automatic-review", title: "Automatic review", state: html `<span data-review-switch="${review.on ? "on" : "off"}"> · ${state}</span>`,
74
+ lead: "One read-only review after each build passes its checks. Only high findings send it back, once.",
75
+ body: view.canChange ? disclose(act, form("review-form", "review", html ``, { label: act, variant: "outline" }, { on: review.on ? "0" : "1" })) : "" });
22
76
  }
23
- const choices = CHECK_LEVELS.map(level => html `<label class="check-choice"><input type="radio" name="level" value="${level}"${level === view.level ? html ` checked` : ""}><span><strong>${CHECK_LEVEL_WORDS[level]}</strong><span class="meta">${CHECK_LEVEL_HINTS[level]}</span></span></label>`);
24
- const levelForm = post("level", html `<fieldset><legend>After each build</legend>${choices}</fieldset>${fallsBack ? html `<p class="meta">No quick command yet, so Quick runs the full check.</p>` : ""}<p class="meta">A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.</p>${password}<button type="submit">Save</button>`, { class: "card check-settings" });
25
- const quickTerms = html `<ul class="check-terms"><li>Runs unattended right after every Quick build, in that build's checkout.</li><li>Plain environment: no credentials.</li><li>A failure shows on the result; nothing is blocked.</li></ul>`;
26
- const quickForm = (value, submit) => post("quick", html `<label>Command<textarea name="command" rows="2" maxlength="2000" spellcheck="false" class="mono" required>${value}</textarea></label><label>Time limit (seconds)<input type="number" name="timeout" min="1" max="3600" value="${view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)}" required></label>${quickTerms}${password}<button type="submit">${submit}</button>`, { class: "check-settings" });
27
- const quick = view.quick === null
28
- ? html `<section class="card check-settings"><h2>Quick check</h2>${view.suggestion === null ? html `<p class="meta">Typecheck and the tests near the change, ideally under 2 minutes.</p>` : html `<p class="meta">Suggested from the project's scripts. Edit it before approving.</p>`}${quickForm(view.suggestion ?? "", "Approve quick check")}</section>`
29
- : html `<section class="card check-settings"><h2>Quick check</h2>${command(view.quick)}<details class="settings-more"><summary>Change quick check</summary>${quickForm(view.quick.command, "Approve change")}</details><details class="settings-more"><summary>Remove quick check</summary>${post("quick-clear", html `<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button>`, { class: "check-settings" })}</details></section>`;
30
- const full = html `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">${view.full === null ? html `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)}</section></details>`;
31
- return html `${head}${levelForm}${batchHtml(view, password)}${reviewHtml(view, password)}${quick}${full}`;
32
- }
33
- /** Batch checks: one state and one sentence; turning it on restates exactly what happens, behind a disclosure with the password. */
34
- function batchHtml(view, password) {
35
- const batch = view.batch;
36
- if (batch === undefined)
37
- return "";
38
- const act = batch.on ? "Turn off" : "Turn on";
39
- const terms = batch.on ? html `<p class="meta">Each result runs its own full check again. Results already waiting are checked now.</p>`
40
- : html `<ul class="check-terms"><li>When results finish within 10 minutes of each other, they're merged in a temporary checkout and the full check runs once.</li><li>If it fails, the batch is split until the result that breaks it is found. Results that conflict are checked on their own.</li><li>Nothing is merged into a real branch. Each result still lands on its own.</li></ul>`;
41
- const change = password === null ? ""
42
- : html `<details class="settings-more"><summary>${act}</summary>${postForm("/settings/checks", html `${terms}${password}<button type="submit">${act}</button>`, { attrs: { class: "check-settings" }, hidden: { repo: view.repo, act: "batch", on: batch.on ? "0" : "1" } })}</details>`;
43
- return html `<section class="card check-settings" data-batch-checks="${batch.on ? "on" : "off"}"><h2>Batch checks</h2><p><strong>${batch.on ? "On" : "Off"}</strong> · ${BATCH_HINT}</p>${batch.on && view.level !== "full" ? html `<p class="meta">Only Full checks are batched.</p>` : ""}${change}</section>`;
44
- }
45
- /** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
46
- function reviewHtml(view, password) {
47
- const review = view.review;
48
- if (review === undefined)
49
- return "";
50
- const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
51
- const act = review.on ? "Turn off" : "Turn on";
52
- const change = password === null ? ""
53
- : html `<details class="settings-more"><summary>${act}</summary>${postForm("/settings/checks", html `${password}<button type="submit">${act}</button>`, { attrs: { class: "check-settings" }, hidden: { repo: view.repo, act: "review", on: review.on ? "0" : "1" } })}</details>`;
54
- return html `<section class="card check-settings" data-review-switch="${review.on ? "on" : "off"}"><h2>Automatic review</h2><p><strong>${state}</strong> · One read-only review after each build passes its checks. Only high findings send it back, once.</p>${change}</section>`;
55
77
  }
78
+ /** The follow-up list's look (result-follow-ups.ts); the checks page itself is kit markup. */
56
79
  export const CHECK_SETTINGS_CSS = `
57
- .check-settings { display: grid; gap: .6rem; max-width: 40rem; }
58
- .check-settings p, .check-settings h2 { margin: 0; }
59
- .check-settings h2 { font-size: 1rem; }
60
- .check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }
61
- .check-settings legend { font-weight: 600; margin-bottom: .25rem; }
62
- .check-settings label { display: grid; gap: .25rem; }
63
- .check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }
64
- .check-settings .check-choice > span { display: grid; gap: .1rem; }
65
- .check-settings .check-choice input { margin-top: .2rem; }
66
- .check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }
67
- .check-settings button { min-height: 44px; justify-self: start; }
68
- .check-settings .check-command { overflow-wrap: anywhere; }
69
- .check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
70
- @media (max-width: 480px) { .check-settings button { justify-self: stretch; } }
71
80
  .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
72
81
  .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }
73
82
  .follow-ups .follow-up-act { margin: 0; }
package/dist/claim.d.ts CHANGED
@@ -103,6 +103,9 @@ export type AcquireResult = {
103
103
  | {
104
104
  ok: false;
105
105
  reason: "coordinator-filed";
106
+ } | {
107
+ ok: false;
108
+ reason: "archived";
106
109
  } | {
107
110
  ok: false;
108
111
  reason: "watch-custody";
package/dist/claim.js CHANGED
@@ -31,7 +31,7 @@ import { randomUUID } from "node:crypto";
31
31
  import { authenticate, drainOf } from "./runner.js";
32
32
  import { pauseForAuth } from "./provider-auth.js";
33
33
  import { DEFAULT_MAX_OPEN_DECISIONS, missingCapability, scopeApprovedForDispatch, taskReadinessBlocker, } from "./dispatch.js";
34
- import { BUILT_IN, } from "./store.js";
34
+ import { BUILT_IN, refArchived, } from "./store.js";
35
35
  import { digestOf } from "./scope.js";
36
36
  import { parseExecutionPlanDocument } from "./plan.js";
37
37
  import { sealUnchangedPlanUnderMode } from "./plan-auto.js";
@@ -109,6 +109,9 @@ function acquireLocked(store, taskRef, runner, options) {
109
109
  if (placedRef !== null && placedRef.coordinatorCid !== null && !store.scopeSealed(placedRef.externalId)) {
110
110
  return { ok: false, reason: "coordinator-filed" };
111
111
  }
112
+ // A PUT-AWAY TASK (v145, task-tidy.md) claims for nobody, on every road: restore it first.
113
+ if (refArchived(store, taskRef))
114
+ return { ok: false, reason: "archived" };
112
115
  // The reservation gate lives HERE, in the one primitive every
113
116
  // acquisition path shares (queue-columns review, finding 1): tick's
114
117
  // acquireIfReady and the raw CLI claim both
@@ -160,6 +160,13 @@ export declare const chatActionRequestSchemas: {
160
160
  readonly flow_trigger_remove: z.ZodObject<{
161
161
  trigger: z.ZodInt;
162
162
  }, z.core.$strict>;
163
+ /** Browser use in the flow's script zones (flow-browser.ts): allowing it takes the person's own password in the secure screen. */
164
+ readonly flow_browser: z.ZodObject<{
165
+ flow: z.ZodInt;
166
+ }, z.core.$strict>;
167
+ readonly flow_browser_off: z.ZodObject<{
168
+ flow: z.ZodInt;
169
+ }, z.core.$strict>;
163
170
  readonly subagent_create: z.ZodObject<{
164
171
  repo: z.ZodString;
165
172
  template: z.ZodOptional<z.ZodUnknown>;
@@ -282,6 +289,8 @@ export declare const sharedActionSchema: z.ZodObject<{
282
289
  operation: z.ZodEnum<{
283
290
  decision_record: "decision_record";
284
291
  decision_retire: "decision_retire";
292
+ flow_browser: "flow_browser";
293
+ flow_browser_off: "flow_browser_off";
285
294
  flow_card_add: "flow_card_add";
286
295
  flow_card_approve: "flow_card_approve";
287
296
  flow_card_assign: "flow_card_assign";
@@ -61,6 +61,9 @@ export const chatActionRequestSchemas = {
61
61
  flow_trigger_pause: z.strictObject({ trigger: id }),
62
62
  flow_trigger_resume: z.strictObject({ trigger: id }),
63
63
  flow_trigger_remove: z.strictObject({ trigger: id }),
64
+ /** Browser use in the flow's script zones (flow-browser.ts): allowing it takes the person's own password in the secure screen. */
65
+ flow_browser: z.strictObject({ flow: id }),
66
+ flow_browser_off: z.strictObject({ flow: id }),
64
67
  subagent_create: z.strictObject({ repo: words, template: z.unknown().optional(), name: z.unknown().optional(), soul: z.unknown().optional() }),
65
68
  subagent_soul: z.strictObject({ subagent: id, soul: words }),
66
69
  subagent_state: z.strictObject({ subagent: id, state: z.enum(["active", "paused", "removed"]) }),