toolroll 0.9.62 → 0.9.65

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 (345) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +66 -8
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +203 -10
  16. package/dist/agent-fence.js +623 -41
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +263 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +199 -17
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/check-levels.js +3 -0
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +20 -1
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +5 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +24 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/cli.d.ts +8 -0
  71. package/dist/contracts/cli.js +3 -1
  72. package/dist/contracts/console-api.d.ts +30 -4
  73. package/dist/contracts/console-api.js +13 -4
  74. package/dist/contracts/flow-card.d.ts +4 -0
  75. package/dist/contracts/flow-card.js +3 -0
  76. package/dist/contracts/flow.d.ts +46 -0
  77. package/dist/contracts/flow.js +11 -2
  78. package/dist/contracts/integration-metadata.d.ts +2 -1
  79. package/dist/contracts/integration-metadata.js +2 -1
  80. package/dist/contracts/lead-tools.d.ts +92 -0
  81. package/dist/contracts/lead-tools.js +13 -0
  82. package/dist/contracts/provider.d.ts +11 -1
  83. package/dist/contracts/provider.js +1 -1
  84. package/dist/contracts/route.d.ts +76 -1
  85. package/dist/contracts/route.js +1 -1
  86. package/dist/contracts/store-json.d.ts +2 -0
  87. package/dist/control-setup.d.ts +1 -1
  88. package/dist/control-ui.js +2 -2
  89. package/dist/converse.d.ts +16 -0
  90. package/dist/converse.js +32 -4
  91. package/dist/demo-chat.js +4 -4
  92. package/dist/demo.js +2 -2
  93. package/dist/desktop-update-gate.js +6 -0
  94. package/dist/discord-chat.js +4 -1
  95. package/dist/dispatch.js +7 -4
  96. package/dist/doctor.d.ts +53 -0
  97. package/dist/doctor.js +427 -0
  98. package/dist/egress-bridge.d.ts +17 -0
  99. package/dist/egress-bridge.js +111 -0
  100. package/dist/egress.d.ts +119 -0
  101. package/dist/egress.js +423 -0
  102. package/dist/evidence-pack.d.ts +1 -1
  103. package/dist/evidence-pack.js +2 -2
  104. package/dist/exec.d.ts +81 -0
  105. package/dist/exec.js +368 -9
  106. package/dist/export-ui.js +2 -2
  107. package/dist/flow-code.d.ts +2 -2
  108. package/dist/flow-code.js +4 -15
  109. package/dist/flow-engine.js +35 -11
  110. package/dist/flow-gallery-ui.js +2 -2
  111. package/dist/flow-gallery.js +27 -8
  112. package/dist/flow-share.js +16 -5
  113. package/dist/flow-triggers.d.ts +84 -11
  114. package/dist/flow-triggers.js +554 -87
  115. package/dist/flow-unattended-words.d.ts +28 -0
  116. package/dist/flow-unattended-words.js +50 -0
  117. package/dist/flow-unattended.d.ts +227 -0
  118. package/dist/flow-unattended.js +776 -0
  119. package/dist/flows-cli.d.ts +28 -1
  120. package/dist/flows-cli.js +63 -12
  121. package/dist/flows-ui.d.ts +2 -3
  122. package/dist/flows-ui.js +56 -5
  123. package/dist/flows.d.ts +4 -0
  124. package/dist/flows.js +9 -1
  125. package/dist/google-mail.d.ts +1 -0
  126. package/dist/google-mail.js +61 -18
  127. package/dist/html.d.ts +9 -0
  128. package/dist/html.js +11 -0
  129. package/dist/integrations-ui.d.ts +2 -1
  130. package/dist/integrations-ui.js +6 -3
  131. package/dist/integrations.d.ts +17 -3
  132. package/dist/integrations.js +38 -18
  133. package/dist/invoke.d.ts +17 -0
  134. package/dist/invoke.js +510 -182
  135. package/dist/keys.d.ts +18 -14
  136. package/dist/keys.js +5 -0
  137. package/dist/landed-cleanup.d.ts +3 -2
  138. package/dist/landed-cleanup.js +12 -5
  139. package/dist/lead-capabilities.js +3 -2
  140. package/dist/lead-contract.js +2 -0
  141. package/dist/lead-doors.d.ts +2 -0
  142. package/dist/lead-doors.js +7 -0
  143. package/dist/lead-follow.d.ts +2 -0
  144. package/dist/lead-follow.js +58 -2
  145. package/dist/lead-progress.js +1 -0
  146. package/dist/lead-queue-runtime.d.ts +46 -0
  147. package/dist/lead-queue-runtime.js +191 -0
  148. package/dist/lead-queue.d.ts +104 -0
  149. package/dist/lead-queue.js +187 -0
  150. package/dist/lead-sweep.d.ts +67 -0
  151. package/dist/lead-sweep.js +99 -0
  152. package/dist/lead-tools.d.ts +22 -0
  153. package/dist/lead-tools.js +163 -4
  154. package/dist/lead.d.ts +13 -5
  155. package/dist/lead.js +89 -29
  156. package/dist/limits-ui.d.ts +8 -1
  157. package/dist/limits-ui.js +26 -0
  158. package/dist/live-bus.d.ts +8 -0
  159. package/dist/live-bus.js +40 -4
  160. package/dist/live-detail.d.ts +27 -0
  161. package/dist/live-detail.js +57 -0
  162. package/dist/live-notes.d.ts +20 -0
  163. package/dist/live-notes.js +47 -0
  164. package/dist/live-records.d.ts +140 -0
  165. package/dist/live-records.js +935 -0
  166. package/dist/live-terminal.d.ts +258 -0
  167. package/dist/live-terminal.js +649 -0
  168. package/dist/live.d.ts +38 -27
  169. package/dist/live.js +142 -124
  170. package/dist/mcp-connect.d.ts +8 -7
  171. package/dist/mcp-connect.js +49 -51
  172. package/dist/mcp-oauth.d.ts +6 -3
  173. package/dist/mcp-oauth.js +8 -4
  174. package/dist/memory-pass.js +2 -2
  175. package/dist/mobile-viewport.d.ts +5 -1
  176. package/dist/mobile-viewport.js +18 -2
  177. package/dist/model-catalog.js +4 -0
  178. package/dist/monitoring-ui.js +2 -2
  179. package/dist/needs-you.d.ts +1 -1
  180. package/dist/needs-you.js +11 -4
  181. package/dist/oauth-run-token.d.ts +51 -0
  182. package/dist/oauth-run-token.js +127 -0
  183. package/dist/operate.d.ts +2 -0
  184. package/dist/operate.js +282 -18
  185. package/dist/passwordless.d.ts +23 -0
  186. package/dist/passwordless.js +26 -0
  187. package/dist/peek-cli.d.ts +12 -4
  188. package/dist/peek-cli.js +19 -11
  189. package/dist/phase-routing.js +10 -3
  190. package/dist/plan-auto.d.ts +11 -0
  191. package/dist/plan-auto.js +62 -11
  192. package/dist/planner-source.d.ts +3 -0
  193. package/dist/planner-source.js +14 -0
  194. package/dist/planner.d.ts +2 -1
  195. package/dist/planner.js +7 -1
  196. package/dist/policy-ui.js +2 -2
  197. package/dist/policy.d.ts +1 -20
  198. package/dist/policy.js +29 -2
  199. package/dist/process-recovery-eligibility.d.ts +1 -1
  200. package/dist/project-access.d.ts +3 -0
  201. package/dist/project-access.js +3 -0
  202. package/dist/project-delete-ui.d.ts +12 -0
  203. package/dist/project-delete-ui.js +41 -9
  204. package/dist/project-delete.d.ts +3 -1
  205. package/dist/project-delete.js +12 -6
  206. package/dist/project-file.d.ts +3 -0
  207. package/dist/project-file.js +16 -0
  208. package/dist/project-memory.d.ts +3 -1
  209. package/dist/project-memory.js +57 -25
  210. package/dist/project-tools.d.ts +103 -27
  211. package/dist/project-tools.js +399 -100
  212. package/dist/provider-auth.d.ts +4 -3
  213. package/dist/provider-auth.js +5 -4
  214. package/dist/provider-connection.d.ts +2 -1
  215. package/dist/provider-connection.js +5 -0
  216. package/dist/provider-home.d.ts +22 -0
  217. package/dist/provider-home.js +99 -0
  218. package/dist/provider.d.ts +42 -5
  219. package/dist/provider.js +97 -19
  220. package/dist/pull-request-ui.js +2 -2
  221. package/dist/push.js +1 -1
  222. package/dist/quick-start.d.ts +24 -0
  223. package/dist/quick-start.js +127 -0
  224. package/dist/release.d.ts +3 -0
  225. package/dist/release.js +14 -8
  226. package/dist/remote-main.d.ts +4 -0
  227. package/dist/remote-main.js +3 -3
  228. package/dist/request-budget-ui.js +2 -2
  229. package/dist/result-review.d.ts +1 -1
  230. package/dist/result-review.js +9 -3
  231. package/dist/retention-ui.js +2 -2
  232. package/dist/retention.js +10 -1
  233. package/dist/run-efficiency.d.ts +73 -0
  234. package/dist/run-efficiency.js +228 -0
  235. package/dist/runner.d.ts +5 -1
  236. package/dist/runner.js +22 -1
  237. package/dist/scope.d.ts +171 -6
  238. package/dist/scope.js +251 -31
  239. package/dist/scout-net.d.ts +4 -1
  240. package/dist/scout-net.js +37 -12
  241. package/dist/scout.d.ts +20 -6
  242. package/dist/scout.js +27 -3
  243. package/dist/serve.js +258 -43
  244. package/dist/server/chat.d.ts +1 -1
  245. package/dist/server/chat.js +122 -31
  246. package/dist/server/chrome.d.ts +6 -1
  247. package/dist/server/chrome.js +18 -7
  248. package/dist/server/console.css +52 -38
  249. package/dist/server/flows.js +65 -13
  250. package/dist/server/guards.d.ts +1 -0
  251. package/dist/server/guards.js +23 -9
  252. package/dist/server/http.d.ts +7 -0
  253. package/dist/server/http.js +3 -2
  254. package/dist/server/live.d.ts +34 -1
  255. package/dist/server/live.js +39 -17
  256. package/dist/server/pages-assets.js +5 -0
  257. package/dist/server/pages.js +90 -13
  258. package/dist/server/people-tokens.d.ts +3 -0
  259. package/dist/server/people-tokens.js +55 -17
  260. package/dist/server/remote-hooks.js +12 -6
  261. package/dist/server/remote.d.ts +2 -1
  262. package/dist/server/remote.js +14 -5
  263. package/dist/server/render-chat.d.ts +12 -3
  264. package/dist/server/render-chat.js +47 -16
  265. package/dist/server/render-pages.d.ts +4 -1
  266. package/dist/server/render-pages.js +5 -4
  267. package/dist/server/render-people.d.ts +3 -2
  268. package/dist/server/render-people.js +3 -2
  269. package/dist/server/render-results.d.ts +18 -2
  270. package/dist/server/render-results.js +37 -28
  271. package/dist/server/render-settings.js +6 -5
  272. package/dist/server/render-tasks.d.ts +35 -10
  273. package/dist/server/render-tasks.js +110 -27
  274. package/dist/server/request-context.d.ts +2 -0
  275. package/dist/server/request-context.js +2 -1
  276. package/dist/server/route-table.d.ts +3 -0
  277. package/dist/server/route-table.js +49 -5
  278. package/dist/server/runtime.d.ts +34 -3
  279. package/dist/server/session.d.ts +25 -1
  280. package/dist/server/session.js +18 -4
  281. package/dist/server/settings.js +132 -13
  282. package/dist/server/sweeps.d.ts +4 -0
  283. package/dist/server/sweeps.js +58 -0
  284. package/dist/server/task-views.d.ts +2 -1
  285. package/dist/server/task-views.js +96 -6
  286. package/dist/server/tasks.js +320 -74
  287. package/dist/server/terminals.d.ts +31 -0
  288. package/dist/server/terminals.js +365 -0
  289. package/dist/setup-guide.js +3 -2
  290. package/dist/slack-chat.js +4 -1
  291. package/dist/spend-ui.js +3 -3
  292. package/dist/spend.js +2 -1
  293. package/dist/steer-capabilities.d.ts +32 -0
  294. package/dist/steer-capabilities.js +60 -0
  295. package/dist/steer-controls.d.ts +14 -0
  296. package/dist/steer-controls.js +36 -0
  297. package/dist/steer-status.js +2 -2
  298. package/dist/storage-sweep-child.js +3 -1
  299. package/dist/storage-sweep-process.d.ts +12 -5
  300. package/dist/storage-sweep-process.js +23 -6
  301. package/dist/storage-ui.js +4 -4
  302. package/dist/store-baseline.d.ts +1 -1
  303. package/dist/store-baseline.js +1 -1
  304. package/dist/store.d.ts +466 -19
  305. package/dist/store.js +1185 -63
  306. package/dist/subagent-desk.js +1 -1
  307. package/dist/subagent-tools.d.ts +1 -1
  308. package/dist/summary.js +3 -0
  309. package/dist/surface.js +6 -4
  310. package/dist/task-source.d.ts +58 -0
  311. package/dist/task-source.js +93 -0
  312. package/dist/team-leads.js +1 -1
  313. package/dist/teams-chat.js +4 -1
  314. package/dist/teardown-git.d.ts +14 -0
  315. package/dist/teardown-git.js +119 -0
  316. package/dist/telegram-lead.d.ts +3 -0
  317. package/dist/telegram-lead.js +44 -7
  318. package/dist/telegram.d.ts +11 -2
  319. package/dist/telegram.js +51 -17
  320. package/dist/terminal-catalog.d.ts +11 -0
  321. package/dist/terminal-catalog.js +129 -0
  322. package/dist/text-limits.d.ts +2 -0
  323. package/dist/text-limits.js +2 -0
  324. package/dist/tool-broker.d.ts +39 -0
  325. package/dist/tool-broker.js +370 -0
  326. package/dist/tool-launcher.js +5 -4
  327. package/dist/tool-secret-store.d.ts +170 -0
  328. package/dist/tool-secret-store.js +834 -0
  329. package/dist/toolroll-update-ui.js +2 -2
  330. package/dist/toolroll-update.js +14 -3
  331. package/dist/tools-ui.d.ts +7 -1
  332. package/dist/tools-ui.js +19 -10
  333. package/dist/webhook-limits.d.ts +23 -0
  334. package/dist/webhook-limits.js +30 -0
  335. package/dist/webhook-readers.d.ts +56 -0
  336. package/dist/webhook-readers.js +138 -0
  337. package/dist/webhook-signing.d.ts +27 -0
  338. package/dist/webhook-signing.js +119 -0
  339. package/dist/work-index.d.ts +29 -0
  340. package/dist/work-index.js +21 -5
  341. package/dist/workspace-revision.js +3 -1
  342. package/dist/worktree.d.ts +10 -4
  343. package/dist/worktree.js +70 -29
  344. package/package.json +13 -2
  345. package/scripts/provider-canary.mjs +210 -31
@@ -269,6 +269,33 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
269
269
  readonly takesValue: true;
270
270
  readonly meaning: "the script's name";
271
271
  }];
272
+ }, {
273
+ readonly action: 'unattended';
274
+ readonly synopsis: "let a flow's own schedule run without approving each card (on: the owner signs, 1 to 7 days; off: any approver revokes)";
275
+ readonly mutation: 'identity-idempotent';
276
+ readonly audience: 'operator';
277
+ readonly positionals: readonly ['flow', 'on|off'];
278
+ readonly flags: readonly [...{
279
+ name: string;
280
+ takesValue: boolean;
281
+ meaning: string;
282
+ }[], {
283
+ readonly name: 'days';
284
+ readonly takesValue: true;
285
+ readonly meaning: "how long it lasts: 1 to 7 days (default 1)";
286
+ }, {
287
+ readonly name: 'budget';
288
+ readonly takesValue: true;
289
+ readonly meaning: 'optional dollar cap per attempt, like 5 or 2.50';
290
+ }, {
291
+ readonly name: 'runs';
292
+ readonly takesValue: true;
293
+ readonly meaning: "optional limit on runs a day (UTC), 1 to 50";
294
+ }, {
295
+ readonly name: 'include';
296
+ readonly takesValue: true;
297
+ readonly meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already';
298
+ }];
272
299
  }, {
273
300
  readonly action: 'archive';
274
301
  readonly synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes';
@@ -284,7 +311,7 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
284
311
  meaning: string;
285
312
  }];
286
313
  }];
287
- /** `--file` is a value here (a path in the project), where elsewhere it is a switch. */
314
+ /** `--file` is a value here (a path in the project), where elsewhere it is a switch; so are unattended's `--budget`, `--runs` and `--include`. */
288
315
  export declare const FLOWS_VALUE_FLAGS: ReadonlySet<string>;
289
316
  export type FlowsCliContext = {
290
317
  store: Store;
package/dist/flows-cli.js CHANGED
@@ -16,9 +16,12 @@ import { saveScript } from './flow-scripts.js';
16
16
  import { exportFlow, fetchFlowFile, FLOW_FILE_MAX_BYTES, FlowFileError, importFlow, parseFlowFile, planFlowImport } from './flow-share.js';
17
17
  import { describeStandingOrder } from './flow-schedule.js';
18
18
  import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, switchOnStarter } from './flow-starters.js';
19
- import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, setFlowTriggerOn, triggerConfigOf, validateTriggerConfig } from './flow-triggers.js';
19
+ import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, setFlowTriggerOn, triggerConfigOf, validateTriggerConfig, rawTakesDeliveries } from './flow-triggers.js';
20
20
  import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps, flowTerms, stepsFor } from './flows.js';
21
+ import { prepareUnattendedCandidates, activeUnattendedGrant, parseUnattendedAsk, revokeUnattended, signUnattended, unattendedViewOf, unattendedWords, untilWords, UNATTENDED_MAX_RUNS } from './flow-unattended.js';
22
+ import { MODE_MAX_DAYS } from './modes.js';
21
23
  import { verifyApproverByPassword } from './principal.js';
24
+ import { proveCredential } from './scope.js';
22
25
  import { projectName } from './project.js';
23
26
  const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
24
27
  const json = { name: 'json', takesValue: false, meaning: 'answer with one machine envelope on stdout' };
@@ -62,10 +65,15 @@ export const FLOWS_DESCRIPTORS = [
62
65
  { action: 'import', synopsis: 'make a flow from a flow file or a gist/GitHub https address; previews in plain words until --yes; triggers arrive off and scripts wait for approval', mutation: 'unkeyed', positionals: ['file'], flags: [...write, yes, repo,
63
66
  { name: 'param', takesValue: true, meaning: 'a value the file asks for, as name=value (repeat for more)' }] },
64
67
  { action: 'script approve', synopsis: 'approve a script an imported flow brought, so it runs', mutation: 'identity-idempotent', flags: [...write, repo, { name: 'name', takesValue: true, meaning: "the script's name" }] },
68
+ { action: 'unattended', synopsis: `let a flow's own schedule run without approving each card (on: the owner signs, 1 to ${MODE_MAX_DAYS} days; off: any approver revokes)`, mutation: 'identity-idempotent', audience: 'operator', positionals: ['flow', 'on|off'], flags: [...write,
69
+ { name: 'days', takesValue: true, meaning: `how long it lasts: 1 to ${MODE_MAX_DAYS} days (default 1)` },
70
+ { name: 'budget', takesValue: true, meaning: 'optional dollar cap per attempt, like 5 or 2.50' },
71
+ { name: 'runs', takesValue: true, meaning: `optional limit on runs a day (UTC), 1 to ${UNATTENDED_MAX_RUNS}` },
72
+ { name: 'include', takesValue: true, meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already' }] },
65
73
  { action: 'archive', synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes', mutation: 'identity-idempotent', positionals: ['flow'], flags: [...write, yes] },
66
74
  ];
67
- /** `--file` is a value here (a path in the project), where elsewhere it is a switch. */
68
- export const FLOWS_VALUE_FLAGS = new Set(['file', 'param']);
75
+ /** `--file` is a value here (a path in the project), where elsewhere it is a switch; so are unattended's `--budget`, `--runs` and `--include`. */
76
+ export const FLOWS_VALUE_FLAGS = new Set(['file', 'param', 'budget', 'runs', 'include']);
69
77
  export async function runFlowsCommand(positional, flags, context) {
70
78
  const [first, second] = positional;
71
79
  const action = first === 'trigger' || first === 'script' || first === 'card' ? `${first} ${second ?? ''}`.trim() : first ?? 'list';
@@ -104,7 +112,7 @@ export async function runFlowsCommand(positional, flags, context) {
104
112
  const flow = flowOf(args[0], context.projects);
105
113
  if (flow === null)
106
114
  return fail('unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
107
- const shown = describeFlow(store, flow);
115
+ const shown = describeFlow(store, flow, context.clock());
108
116
  return ok({ flow: shown }, showLines(shown));
109
117
  }
110
118
  if (action === 'export') {
@@ -197,7 +205,7 @@ export async function runFlowsCommand(positional, flags, context) {
197
205
  if (!switched.ok)
198
206
  return refuse(project, 'refused', switched.said);
199
207
  record(project, 'accepted', `flow #${switched.flow}`);
200
- return ok({ applied: true, flow: describeFlow(store, store.getFlow(switched.flow)) }, [`${switched.said} Flow #${switched.flow}.`]);
208
+ return ok({ applied: true, flow: describeFlow(store, store.getFlow(switched.flow), context.clock()) }, [`${switched.said} Flow #${switched.flow}.`]);
201
209
  }
202
210
  const template = templateId === null ? null : FLOW_TEMPLATES.find(one => one.id === templateId) ?? null;
203
211
  if (templateId !== null && template === null)
@@ -250,7 +258,7 @@ export async function runFlowsCommand(positional, flags, context) {
250
258
  return refuse(project, 'invalid-trigger', error instanceof Error ? error.message : 'Its trigger isn\'t valid.');
251
259
  }
252
260
  record(project, 'accepted', `flow #${id}`);
253
- return ok({ applied: true, flow: describeFlow(store, store.getFlow(id)) }, [`Created ${flowName}, flow #${id}. Add cards with toolroll flows card add ${id} --title "…".`]);
261
+ return ok({ applied: true, flow: describeFlow(store, store.getFlow(id), context.clock()) }, [`Created ${flowName}, flow #${id}. Add cards with toolroll flows card add ${id} --title "…".`]);
254
262
  }
255
263
  if (action === 'script save') {
256
264
  const given = text('repo');
@@ -328,7 +336,7 @@ export async function runFlowsCommand(positional, flags, context) {
328
336
  return refuse(project, 'invalid-file', error instanceof Error ? error.message : 'That flow couldn\'t be imported.');
329
337
  }
330
338
  record(project, 'accepted', `flow #${made.id}`);
331
- return ok({ applied: true, flow: describeFlow(store, store.getFlow(made.id)), said: made.said }, [`${made.said} Flow #${made.id}.`]);
339
+ return ok({ applied: true, flow: describeFlow(store, store.getFlow(made.id), context.clock()), said: made.said }, [`${made.said} Flow #${made.id}.`]);
332
340
  }
333
341
  if (action === 'script approve') {
334
342
  const given = text('repo'), name = text('name');
@@ -352,6 +360,33 @@ export async function runFlowsCommand(positional, flags, context) {
352
360
  if (flow === null)
353
361
  return refuse(null, 'unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
354
362
  const definition = flowDefinitionOf(flow);
363
+ if (action === 'unattended') {
364
+ const flow = flowOf(args[0], reachable);
365
+ if (flow === null)
366
+ return refuse(null, 'unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
367
+ const turn = args[1];
368
+ if (turn !== 'on' && turn !== 'off')
369
+ return fail('usage', `Use flows unattended ${flow.id} on --days N, or flows unattended ${flow.id} off.`, EXIT.usage);
370
+ if (turn === 'off') {
371
+ // Lowering authority is one step for any approver on the project.
372
+ if (!revokeUnattended(store, flow.id, who, now))
373
+ return refuse(flow.repo, 'not-on', `${flow.name} isn't running on its own. Nothing was changed.`);
374
+ record(flow.repo, 'accepted', `flow #${flow.id} off`);
375
+ return ok({ applied: true, unattended: null }, [`${flow.name} no longer runs on its own. Its next cards wait for you; running work is untouched.`]);
376
+ }
377
+ const asked = parseUnattendedAsk({ days: text('days') ?? '1', budget: text('budget') ?? '', runs: text('runs') ?? '', include: text('include') ?? '' });
378
+ if (!asked.ok)
379
+ return fail('usage', asked.message, EXIT.usage);
380
+ const signed = await signUnattended(store, { flow: flow.id, signer: who, ask: asked.ask }, now);
381
+ if (!signed.ok)
382
+ return refuse(flow.repo, 'refused', `${signed.message} Nothing was changed.`);
383
+ record(flow.repo, 'accepted', `flow #${flow.id} on until ${signed.grant.absoluteExpiry}`);
384
+ return ok({ applied: true, unattended: unattendedJson(store, flow, now) }, [
385
+ `${flow.name} runs on its own until ${untilWords(signed.grant.absoluteExpiry)}. Signed by ${who}:`,
386
+ ...unattendedWords(signed.grant.terms).map(line => ` ${line}`),
387
+ `Revoke: toolroll flows unattended ${flow.id} off`,
388
+ ]);
389
+ }
355
390
  if (action === 'archive') {
356
391
  const cards = store.flowCards(flow.id, false).length, triggers = store.flowTriggers(flow.id).filter(one => one.state === 'active').length;
357
392
  if (!flags.has('yes'))
@@ -395,7 +430,7 @@ export async function runFlowsCommand(positional, flags, context) {
395
430
  return refuse(flow.repo, 'stale', 'Someone else changed this flow. Look again, then make yours.');
396
431
  record(flow.repo, 'accepted', `flow #${flow.id}`);
397
432
  settle(flow.repo);
398
- return ok({ applied: true, flow: describeFlow(store, store.getFlow(flow.id)) }, [`Saved ${name}.`]);
433
+ return ok({ applied: true, flow: describeFlow(store, store.getFlow(flow.id), context.clock()) }, [`Saved ${name}.`]);
399
434
  }
400
435
  if (action === 'card add') {
401
436
  const title = text('title');
@@ -468,9 +503,15 @@ export async function runFlowsCommand(positional, flags, context) {
468
503
  const zone = definition.stages.find(one => one.id === (config.zone ?? definition.start))?.title ?? 'the first zone';
469
504
  return preview(flow.repo, `Add a trigger to ${flow.name}`, [words, `Cards start in ${zone}.`, 'Work a card starts still waits for your usual approvals.'], { flowId: flow.id, trigger: config });
470
505
  }
471
- const made = addFlowTriggerTo(store, flow, input.value, who, now, context.configDir);
506
+ // A trigger that takes deliveries mints an address: a credential act, a password typed for this command (never the
507
+ // remembered login under Passwordless) (docs/plans/passwordless.md).
508
+ const ingress = rawTakesDeliveries(input.value);
509
+ if (ingress && !proveCredential(store, acting.name, acting.token).ok)
510
+ return refuse(flow.repo, 'unauthenticated', 'Making a webhook address takes your password, typed: pass --token.');
511
+ const made = addFlowTriggerTo(store, flow, input.value, who, now, context.configDir, ingress ? { ingress: 'proved' } : {});
472
512
  if (!made.ok)
473
513
  return refuse(flow.repo, 'invalid-trigger', made.message);
514
+ await prepareUnattendedCandidates(store, flow.id);
474
515
  record(flow.repo, 'accepted', `flow #${flow.id} trigger #${made.id}`);
475
516
  settle(flow.repo);
476
517
  // A webhook's address (and GitHub's signing secret) is shown this once, never stored readable.
@@ -490,9 +531,9 @@ export async function runFlowsCommand(positional, flags, context) {
490
531
  return checked.ok ? ok({ applied: true, triggerId: trigger.id, said: checked.said }, [checked.said]) : fail('check-failed', checked.said);
491
532
  }
492
533
  if (action === 'trigger remove')
493
- removeFlowTrigger(store, trigger, now, context.configDir);
534
+ removeFlowTrigger(store, trigger, now, context.configDir, who);
494
535
  else
495
- setFlowTriggerOn(store, trigger, action !== 'trigger pause', now);
536
+ setFlowTriggerOn(store, trigger, action !== 'trigger pause', now, who);
496
537
  record(flow.repo, 'accepted', `flow #${flow.id} trigger #${trigger.id}`);
497
538
  settle(flow.repo);
498
539
  const said = action === 'trigger remove' ? 'Trigger removed.' : action === 'trigger pause' ? 'Trigger paused.' : 'Trigger on again.';
@@ -524,7 +565,7 @@ function zoneOf(stage, flow) {
524
565
  function cardOf(definition, card) {
525
566
  return { id: card.id, title: card.title, zone: card.stage, zoneTitle: definition?.stages.find(one => one.id === card.stage)?.title ?? card.stage, state: card.state, waiting: card.waiting, task: card.task ?? card.primaryTask, updatedAt: card.updatedAt };
526
567
  }
527
- function describeFlow(store, flow) {
568
+ function describeFlow(store, flow, now) {
528
569
  const definition = flowDefinitionOf(flow);
529
570
  return {
530
571
  id: flow.id, name: flow.name, repo: flow.repo, project: projectName(flow.repo), owner: flow.owner, revision: flow.revision, readable: definition !== null,
@@ -537,14 +578,24 @@ function describeFlow(store, flow) {
537
578
  ...(config?.kind === 'schedule' && config.order !== undefined ? { order: describeStandingOrder(config.order, flow.repo, config.schedule) } : {}) };
538
579
  }),
539
580
  cards: store.flowCards(flow.id, true).slice(0, 10).map(card => cardOf(definition, card)),
581
+ unattended: unattendedJson(store, flow, now),
540
582
  };
541
583
  }
584
+ /** Whether a flow runs on its own, and until when: null when it doesn't. */
585
+ function unattendedJson(store, flow, now) {
586
+ const live = activeUnattendedGrant(store, flow.id, now) === null ? null : unattendedViewOf(store, flow, { name: flow.owner, approver: true }, now).active;
587
+ return live === null ? null : { until: live.until, signedBy: live.signedBy, budgetMicrousd: live.budgetMicrousd, dailyRunCap: live.dailyRunCap, runsToday: live.runsToday, covers: live.covers };
588
+ }
542
589
  function showLines(flow) {
543
590
  const title = (id) => id === null ? null : flow.zones.find(one => one.id === id)?.title ?? id;
544
591
  if (!flow.readable)
545
592
  return [`#${flow.id} ${flow.name} · ${flow.project}`, 'This flow\'s drawing can\'t be read. Save it again from the editor.'];
546
593
  return [
547
594
  `#${flow.id} ${flow.name} · ${flow.project} · owner ${flow.owner}`,
595
+ flow.unattended === null
596
+ ? `Runs on its own: no · turn on: toolroll flows unattended ${flow.id} on --days N`
597
+ : `Runs on its own until ${untilWords(flow.unattended.until)} · revoke: toolroll flows unattended ${flow.id} off`,
598
+ ...(flow.unattended === null ? [] : [` Covers: ${flow.unattended.covers.join('; ')}`]),
548
599
  '', 'Zones',
549
600
  ...flow.zones.flatMap((zone, index) => [
550
601
  ` ${index + 1}. ${zone.title} (${zone.id}) — ${zone.does}${zone.id === flow.start ? ' · start' : ''}`,
@@ -1,5 +1,3 @@
1
- /** Flows in the console: the list of a person's flows, and one flow's canvas
2
- * (the React view's data, plus a plain fallback page the view replaces). */
3
1
  import type { BrowserFlowView } from "./browser-workspace.js";
4
2
  import type { FlowRow, Store } from "./store.js";
5
3
  import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
@@ -29,7 +27,8 @@ export declare function flowView(store: Store, flow: FlowRow, viewer: {
29
27
  repos: readonly string[];
30
28
  startTrigger?: number | null;
31
29
  sortReady?: boolean;
32
- toolHome?: string;
30
+ toolHome?: string | undefined;
31
+ now?: Date;
33
32
  }): BrowserFlowView;
34
33
  /** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
35
34
  export declare function flowFallbackHtml(view: BrowserFlowView): Html;
package/dist/flows-ui.js CHANGED
@@ -1,6 +1,12 @@
1
+ /** Flows in the console: the list of a person's flows, and one flow's canvas
2
+ * (the React view's data, plus a plain fallback page the view replaces). */
3
+ import { liveAttemptOf, steerControlsFor } from "./steer-controls.js";
1
4
  import { draftFor, flowDefinitionOf } from "./flow-engine.js";
2
5
  import { describeStandingOrder } from "./flow-schedule.js";
3
- import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
6
+ import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, senderOf, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
7
+ import { SENDER_WORDS } from "./webhook-signing.js";
8
+ import { READER_CAPTURED } from "./webhook-readers.js";
9
+ import { hostOf, linkWords, provedWords, snapshotOf } from "./task-source.js";
4
10
  import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
5
11
  import { flowInsights, troubleWords } from "./flow-insights.js";
6
12
  import { parseSortDecision, sortChip } from "./flow-sort.js";
@@ -16,9 +22,44 @@ import { labelOf, nameOf } from "./subagent-admin.js";
16
22
  import { callWords, receiptWords } from "./subagent-tools.js";
17
23
  import { undoFor } from "./subagent-week.js";
18
24
  import { flowCardTaskLine } from "./flow-card-task.js";
25
+ import { unattendedViewOf } from "./flow-unattended.js";
19
26
  import { readFlowSecrets } from "./flow-secrets.js";
20
27
  import { html, joinHtml, postForm } from "./html.js";
21
28
  const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
29
+ /** v126: what happens to deliveries turned away (refused with 503), as far as each sender is known to say: nothing
30
+ * here claims a loss that a sender's own retries may still make good. */
31
+ function resendWords(config) {
32
+ if (config.kind === "github")
33
+ return "GitHub doesn't retry on its own: resend them from the repository's Settings → Webhooks → this webhook → Recent Deliveries → Redeliver.";
34
+ if (config.kind === "linear")
35
+ return "Linear may retry a refused delivery. Look in the webhook's settings in Linear for its recent deliveries; any it doesn't resend can't be recovered here.";
36
+ const sender = config.kind === "webhook" ? senderOf(config) : "none";
37
+ return sender === "vercel" ? "Vercel retries refused deliveries for up to a day, so some may still arrive. Its webhook's delivery attempts are listed in Vercel's webhook settings; any it gives up on are lost."
38
+ : sender === "sentry" ? "Sentry may retry a refused delivery. Your Internal Integration's Dashboard in Sentry (Developer Settings) shows its request log; any it doesn't resend are lost, though new alerts for the same issues still arrive."
39
+ : "The sender may retry. Look in its webhook delivery log and resend from there if it offers it; any it doesn't resend are lost.";
40
+ }
41
+ /** v126: what a webhook trigger's row says about how its deliveries are proved. */
42
+ function hookView(trigger, config, dir) {
43
+ const ready = hookReady(trigger, dir);
44
+ const dropped = trigger.droppedCount > 0 && trigger.droppedSince !== null ? { count: trigger.droppedCount, since: trigger.droppedSince, resend: resendWords(config) } : null;
45
+ if (config.kind !== "webhook")
46
+ return { ready, needsSecret: config.kind === "linear", sender: null, makesSecret: false, warning: null, readerNote: null, dropped, scope: null };
47
+ const sender = senderOf(config);
48
+ // What it needs first, first: an unset secret refuses everything, so that is said before any narrowing.
49
+ const warning = trigger.hookHash === null && (sender === "none" || ready) ? "No address yet: choose New address to make one and give it to the sender."
50
+ : sender === "none" ? "Unsigned: anyone with its address can add cards. Choose who signs its deliveries to close that."
51
+ : !ready ? `Not set up: its deliveries are refused until ${sender === "sentry" || sender === "vercel" ? `${SENDER_WORDS[sender]}'s secret is pasted (with New address for the address to give it)` : "it is set up"}.`
52
+ : (sender === "sentry" || sender === "vercel") && config.project == null ? `Any ${SENDER_WORDS[sender]} project this secret covers can add cards. Name the project to narrow it.`
53
+ : null;
54
+ // A sender that signs only the body: byte-identical requests are one delivery (§3).
55
+ const repeats = sender === "github-style" ? "Identical requests count once. If the sender must send the same request again and have it count, use Standard Webhooks." : null;
56
+ // A reader written from its sender's documentation, not yet from a captured delivery, says so on its own line,
57
+ // whatever else the row says (webhook-readers.ts).
58
+ const readerNote = (sender === "sentry" || sender === "vercel") && !READER_CAPTURED[sender]
59
+ ? `Reader unverified: Toolroll reads ${SENDER_WORDS[sender]} deliveries from ${SENDER_WORDS[sender]}'s documented examples; no real delivery has been checked yet.` : null;
60
+ return { ready, needsSecret: sender === "sentry" || sender === "vercel", sender, makesSecret: sender === "standard" || sender === "github-style", warning, readerNote: [readerNote, repeats].filter(one => one !== null).join(" ") || null, dropped,
61
+ scope: sender === "sentry" || sender === "vercel" ? { project: config.project ?? null, team: config.team ?? null } : null };
62
+ }
22
63
  export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:"";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}`;
23
64
  /** The flows a person can open, with how many cards wait in each, and the new-flow form. */
24
65
  export function flowsListHtml(store, flows, projects, canCreate, problem) {
@@ -141,14 +182,22 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
141
182
  id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
142
183
  waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
143
184
  ? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable" : card.waiting,
144
- task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}` },
185
+ task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}`,
186
+ // Its running attempt's composer, for an approver who may read the task (steer-controls.ts).
187
+ ...(() => {
188
+ const ref = card.state === "active" && canReadTask && viewer.approver ? store.lookupRef(task) : null;
189
+ const run = ref === null ? null : liveAttemptOf(store, ref.id, now);
190
+ const composer = run === null ? null : steerControlsFor(store, task, run, now, viewer.approver);
191
+ return composer === null ? {} : { composer };
192
+ })() },
145
193
  createdBy: card.createdBy, updatedAt: card.updatedAt,
146
194
  canDecide,
147
195
  // What a "Send to me" sent shows once, in its own box (sent), not again here; a report zone's items and full report
148
196
  // (`<id>.items`, `<id>.report`) are for later zones, and its summary is shown.
149
197
  outputs: Object.entries(card.outputs).filter(([id]) => !id.includes(".") && stages.find(one => one.id === id)?.kind !== "send").map(([id, text]) => ({ stage: id, title: title(id), text })),
150
198
  history: [...moves, ...owned].sort((a, b) => b.at.localeCompare(a.at)).slice(0, 30),
151
- source: card.source,
199
+ // v126: an outside event's card says how it was proved and where its link goes, from what was kept at receipt.
200
+ source: card.source === null ? null : (() => { const snapshot = snapshotOf(card.source); return snapshot === null ? card.source : { ...card.source, proved: provedWords(snapshot), host: hostOf(card.source.url), shown: card.source.url === null ? null : linkWords(card.source.url) }; })(),
152
201
  owner: card.owner, watchers, watching: watchers.includes(viewer.name),
153
202
  mine: card.owner === viewer.name || watchers.includes(viewer.name) || canDecide || choosing?.person === viewer.name,
154
203
  comments: discussion.filter(one => one.kind === "comment").map(one => ({ id: one.id, author: one.author, body: one.body, mentions: one.mentions, at: one.at })),
@@ -177,7 +226,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
177
226
  ...(config === null ? { name: "Trigger", detail: "Can't be read" } : triggerHeadline(config, store)),
178
227
  zone: title(config?.zone ?? definition?.start ?? ""), zoneId: config?.zone ?? definition?.start ?? "", state: trigger.state, status: trigger.lastOutcome, statusLink: planeStatusLink(config, trigger.lastOutcome), statusAt: trigger.lastAt, failing: trigger.failures > 0,
179
228
  button: config?.kind === "button" ? { label: config.label, questions: config.questions } : null,
180
- hook: config !== null && takesDeliveries(config) ? { ready: hookReady(trigger, setup.dir), needsSecret: config.kind === "linear" } : null,
229
+ hook: config !== null && takesDeliveries(config) ? hookView(trigger, config, setup.dir) : null,
181
230
  checkable: ((config?.kind === "github" || config?.kind === "linear") && config.delivery === "poll") || config?.kind === "email" || (config?.kind === "schedule" && config.script !== undefined) || config?.kind === "plane-review",
182
231
  shared: config?.kind === "button" && trigger.hookHash !== null,
183
232
  ...(config?.kind === "schedule" && config.order !== undefined ? { order: describeStandingOrder(config.order, flow.repo, config.schedule) } : {}),
@@ -211,6 +260,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
211
260
  selectedCard,
212
261
  live: flowFingerprint(store, flow.id),
213
262
  canEdit: viewer.approver,
263
+ unattended: unattendedViewOf(store, flow, viewer, setup.now ?? now),
214
264
  approvers: store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, flow.repo)),
215
265
  projects: [flow.repo, ...setup.repos.filter(one => one !== flow.repo)].map(path => ({ path, name: projectName(path) })),
216
266
  kinds: FLOW_STAGE_KINDS.map(kind => ({ kind, label: FLOW_KIND_WORDS[kind].label, about: FLOW_KIND_WORDS[kind].about })),
@@ -219,7 +269,8 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
219
269
  }
220
270
  /** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
221
271
  export function flowFallbackHtml(view) {
222
- return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p><ol>${view.stages.map(stage => {
272
+ const own = view.unattended.active;
273
+ return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p>${own !== null && html `<p data-flow-unattended>Runs on its own until ${own.untilWords}</p>`}<ol>${view.stages.map(stage => {
223
274
  const cards = view.cards.filter(card => card.stage === stage.id && card.state === "active");
224
275
  return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
225
276
  })}</ol></section>`;
package/dist/flows.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type SourceLike } from "./task-source.js";
1
2
  import { type ContractIssue, type ContractResult } from "./contracts/contract.js";
2
3
  import { CHOICES_MAX, CHOICES_MIN, FLOW_COLORS, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, LONGEST_WAIT_MINUTES, SCRIPT_LANGUAGES, SCRIPT_NAME, sortKeyOf, type FlowDefinition, type FlowStage, type FlowStageKind, type FlowStepFields, type FlowWait, type ScriptLanguage, type TriggerInput } from "./contracts/flow.js";
3
4
  export { CHOICES_MAX, CHOICES_MIN, FLOW_COLORS, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, LONGEST_WAIT_MINUTES, SCRIPT_LANGUAGES, SCRIPT_NAME, sortKeyOf };
@@ -66,11 +67,13 @@ export declare function choiceTargets(stage: Pick<FlowStage, "options">): string
66
67
  export declare function replyTarget(stage: Pick<FlowStage, "options" | "onFail">): string | null;
67
68
  /** What a card's work is held to: the zones' steps and paths, never where they sit on the canvas. */
68
69
  export declare function flowDigest(definition: FlowDefinition): string;
70
+ /** `source` (v126): where the card came from, when an outside event's delivery made it ({{card.source}}). */
69
71
  type FlowFillCard = {
70
72
  title: string;
71
73
  description: string | null;
72
74
  note: string | null;
73
75
  outputs: Record<string, string>;
76
+ source?: SourceLike | null;
74
77
  };
75
78
  /** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
76
79
  export declare function fillFlowText(template: string, card: FlowFillCard, encode?: (value: string) => string): string;
@@ -83,6 +86,7 @@ export declare const flowAttachedMark: (label: string) => string;
83
86
  export declare function flowWorkTemplate(instructions: string, card: {
84
87
  description: string | null;
85
88
  note: string | null;
89
+ source?: SourceLike | null;
86
90
  }): string;
87
91
  /** How a filled-in value is named where it is attached: the same words flowGoalCuts labels it with. */
88
92
  export declare function flowValueLabel(key: string, stageTitle?: (id: string) => string): string;
package/dist/flows.js CHANGED
@@ -50,6 +50,7 @@
50
50
  import { TEXT_LIMITS } from "./text-limits.js";
51
51
  import { createHash } from "node:crypto";
52
52
  import { projectName } from "./project.js";
53
+ import { cardReference } from "./task-source.js";
53
54
  import { parseContract, readVersioned } from "./contracts/contract.js";
54
55
  import { stageReferenceProblems } from "./contracts/stage-output.js";
55
56
  import { CHOICES_MAX, CHOICES_MIN, FLOW_ALIASES, FLOW_COLORS, FLOW_DEFINITION_VERSION, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, flowDefinitionSchema, flowStepsSchema, HEADERS_MAX, legacySubagentStep, LONGEST_WAIT_MINUTES, savedFlowDefinitionSchema, SCRIPT_LANGUAGES, SCRIPT_NAME, sortKeyOf, SURE_AT_MAX, SURE_AT_MIN, ZONE_ID, } from "./contracts/flow.js";
@@ -477,7 +478,7 @@ export function flowDigest(definition) {
477
478
  return createHash("sha256").update(JSON.stringify({ start: definition.start, terms })).digest("hex").slice(0, 32);
478
479
  }
479
480
  /** {{stage.<id>}} is an earlier zone's output; a report zone also fills {{stage.<id>.items}} and {{stage.<id>.report}}. */
480
- const FILLED = /\{\{\s*(card\.title|card\.description|card\.email|note|stage\.([a-z0-9-]+(?:\.(?:items|report))?))\s*\}\}/g;
481
+ const FILLED = /\{\{\s*(card\.title|card\.description|card\.email|card\.source|note|stage\.([a-z0-9-]+(?:\.(?:items|report))?))\s*\}\}/g;
481
482
  /** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
482
483
  export function fillFlowText(template, card, encode = value => value) {
483
484
  return template.replace(FILLED, (_match, key, stage) => encode(fillOne(key, stage, card))).trim();
@@ -494,6 +495,9 @@ export function flowWorkTemplate(instructions, card) {
494
495
  ...(names("card.title") || names("card.description") ? [] : [`The card: {{card.title}}${card.description ? "\n\n{{card.description}}" : ""}`]),
495
496
  ...(names("card.title") && !names("card.description") && card.description ? ["Details on the card:\n{{card.description}}"] : []),
496
497
  ...(names("note") || !card.note ? [] : ["Changes asked for: {{note}}"]),
498
+ // v126: an outside event's card says where it came from — a filled value like the others, so it is fitted into the
499
+ // goal with them (whole when it fits, attached when it doesn't), never at the cost of the zone's own words.
500
+ ...(names("card.source") || cardReference(card.source) === "" ? [] : ["{{card.source}}"]),
497
501
  ];
498
502
  return [instructions, ...extra].join("\n\n");
499
503
  }
@@ -507,6 +511,8 @@ export function flowValueLabel(key, stageTitle = id => id) {
507
511
  return "The card's description";
508
512
  if (key === "card.email")
509
513
  return "The card's email address";
514
+ if (key === "card.source")
515
+ return "Where the card came from";
510
516
  if (key === "note")
511
517
  return "The note it was sent back with";
512
518
  const match = /^stage\.([a-z0-9-]+)(?:\.(items|report))?$/.exec(key);
@@ -556,6 +562,8 @@ function fillOne(key, stage, card) {
556
562
  {
557
563
  if (key === "card.title")
558
564
  return card.title;
565
+ if (key === "card.source")
566
+ return cardReference(card.source);
559
567
  if (key === "card.description")
560
568
  return card.description ?? "";
561
569
  if (key === "card.email")
@@ -9,6 +9,7 @@ export type GoogleMail = {
9
9
  refreshToken: string | null;
10
10
  address: string | null;
11
11
  };
12
+ /** The saved client and connection; null when there's none. A keychain that refuses throws (SecretStoreError). */
12
13
  export declare function readGoogleMail(dir: string | null): GoogleMail | null;
13
14
  /** Connected: a refresh token and the address it belongs to. */
14
15
  export declare function googleConnected(dir: string | null): {
@@ -7,27 +7,31 @@
7
7
  * and pastes its ID and secret into Settings → Email. Connecting sends them
8
8
  * to Google's consent screen; Google sends them back here with a one-time
9
9
  * code (checked against a state and a PKCE verifier made for that visit),
10
- * which becomes a refresh token. The client secret and refresh token live
11
- * in a 0600 file beside the database, never in a column, a log or a page.
10
+ * which becomes a refresh token. The client secret and refresh token are
11
+ * kept by Toolroll (tool-secret-store.ts: the keychain, or an owner-only file
12
+ * beside the database where there's none), never in a column, a log or a page.
12
13
  * Mail itself goes over IMAP and SMTP with XOAUTH2, so reading and sending
13
14
  * are the same code as for any other mail server.
14
15
  */
15
16
  import { createHash, randomBytes } from "node:crypto";
16
- import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
- import { join } from "node:path";
18
- const FILE = "google-mail.json";
17
+ import { SecretStoreError, readHeldSecret, writeHeldSecret } from "./tool-secret-store.js";
18
+ const HELD = { name: "google-mail", file: ["google-mail.json"] };
19
19
  export const GOOGLE_AUTH = "https://accounts.google.com/o/oauth2/v2/auth";
20
20
  export const GOOGLE_TOKEN = "https://oauth2.googleapis.com/token";
21
21
  const GOOGLE_REVOKE = "https://oauth2.googleapis.com/revoke";
22
22
  /** Full mail access is what IMAP and SMTP sign-in needs; "email" says which address it is. */
23
23
  export const GOOGLE_SCOPE = "https://mail.google.com/ openid email";
24
24
  export const GOOGLE_CALLBACK = "/settings/google/callback";
25
+ /** The saved client and connection; null when there's none. A keychain that refuses throws (SecretStoreError). */
25
26
  export function readGoogleMail(dir) {
26
- if (dir === null || !existsSync(join(dir, FILE)))
27
+ if (dir === null)
28
+ return null;
29
+ const text = readHeldSecret(dir, HELD.name, HELD.file);
30
+ if (text === null)
27
31
  return null;
28
32
  try {
29
- const raw = JSON.parse(readFileSync(join(dir, FILE), "utf8"));
30
- if (typeof raw["clientId"] !== "string" || typeof raw["clientSecret"] !== "string")
33
+ const raw = JSON.parse(text);
34
+ if (raw === null || typeof raw["clientId"] !== "string" || typeof raw["clientSecret"] !== "string")
31
35
  return null;
32
36
  return { clientId: raw["clientId"], clientSecret: raw["clientSecret"], refreshToken: typeof raw["refreshToken"] === "string" ? raw["refreshToken"] : null, address: typeof raw["address"] === "string" ? raw["address"] : null };
33
37
  }
@@ -37,23 +41,39 @@ export function readGoogleMail(dir) {
37
41
  }
38
42
  /** Connected: a refresh token and the address it belongs to. */
39
43
  export function googleConnected(dir) {
40
- const saved = readGoogleMail(dir);
44
+ let saved;
45
+ try {
46
+ saved = readGoogleMail(dir);
47
+ }
48
+ catch {
49
+ saved = null;
50
+ }
41
51
  return saved?.refreshToken && saved.address ? { address: saved.address } : null;
42
52
  }
43
53
  function write(dir, value) {
44
- writeFileSync(join(dir, FILE), JSON.stringify(value), { mode: 0o600 });
45
- chmodSync(join(dir, FILE), 0o600);
54
+ writeHeldSecret(dir, HELD.name, HELD.file, JSON.stringify(value));
46
55
  }
47
56
  /** Save the OAuth client from Settings; an empty secret keeps the saved one. A new client forgets the old connection. */
48
57
  export function saveGoogleClient(dir, input) {
49
58
  const clientId = typeof input.clientId === "string" ? input.clientId.trim() : "";
50
59
  if (!/^[0-9]{6,20}-[a-z0-9]{8,64}\.apps\.googleusercontent\.com$/.test(clientId))
51
60
  return { ok: false, message: "That doesn't look like a Google OAuth client ID. It ends in .apps.googleusercontent.com." };
52
- const before = readGoogleMail(dir);
61
+ let before;
62
+ try {
63
+ before = readGoogleMail(dir);
64
+ }
65
+ catch (error) {
66
+ return { ok: false, message: error instanceof SecretStoreError ? error.message : "The saved client couldn't be read." };
67
+ }
53
68
  const clientSecret = typeof input.clientSecret === "string" && input.clientSecret.trim() !== "" ? input.clientSecret.trim() : before?.clientId === clientId ? before.clientSecret : "";
54
69
  if (!/^[A-Za-z0-9_-]{16,80}$/.test(clientSecret))
55
70
  return { ok: false, message: "Paste the client secret from the same OAuth client." };
56
- write(dir, { clientId, clientSecret, refreshToken: before?.clientId === clientId ? before.refreshToken : null, address: before?.clientId === clientId ? before.address : null });
71
+ try {
72
+ write(dir, { clientId, clientSecret, refreshToken: before?.clientId === clientId ? before.refreshToken : null, address: before?.clientId === clientId ? before.address : null });
73
+ }
74
+ catch (error) {
75
+ return { ok: false, message: error instanceof SecretStoreError ? error.message : "The client couldn't be saved." };
76
+ }
57
77
  return { ok: true };
58
78
  }
59
79
  /** Where to send the person to consent, and what to remember to check their return against. */
@@ -89,7 +109,13 @@ function addressOf(idToken) {
89
109
  }
90
110
  /** The one-time code from Google's redirect, exchanged for a refresh token and saved. */
91
111
  export async function finishGoogleConsent(dir, visit, code, fetcher) {
92
- const client = readGoogleMail(dir);
112
+ let client;
113
+ try {
114
+ client = readGoogleMail(dir);
115
+ }
116
+ catch (error) {
117
+ return { ok: false, message: error instanceof SecretStoreError ? error.message : "The saved client couldn't be read." };
118
+ }
93
119
  if (client === null)
94
120
  return { ok: false, message: "Save the Google OAuth client first." };
95
121
  let answer;
@@ -107,14 +133,25 @@ export async function finishGoogleConsent(dir, visit, code, fetcher) {
107
133
  const address = addressOf(answer.body["id_token"]);
108
134
  if (address === null)
109
135
  return { ok: false, message: "Google didn't say which address this is. Connect again." };
110
- write(dir, { ...client, refreshToken: refresh, address });
136
+ try {
137
+ write(dir, { ...client, refreshToken: refresh, address });
138
+ }
139
+ catch (error) {
140
+ return { ok: false, message: error instanceof SecretStoreError ? error.message : "The connection couldn't be saved." };
141
+ }
111
142
  cached.delete(client.clientId);
112
143
  return { ok: true, address };
113
144
  }
114
145
  const cached = new Map();
115
146
  /** A fresh access token for IMAP and SMTP (kept in memory until a minute before it ends). */
116
147
  export async function googleAccessToken(dir, fetcher, now = Date.now()) {
117
- const saved = readGoogleMail(dir);
148
+ let saved;
149
+ try {
150
+ saved = readGoogleMail(dir);
151
+ }
152
+ catch (error) {
153
+ return { ok: false, said: error instanceof SecretStoreError ? error.message : "The Google connection couldn't be read.", permanent: false };
154
+ }
118
155
  if (saved?.refreshToken == null || saved.address === null)
119
156
  return { ok: false, said: "The Google account isn't connected. Connect it in Settings → Email.", permanent: true };
120
157
  const hit = cached.get(saved.clientId);
@@ -138,7 +175,13 @@ export async function googleAccessToken(dir, fetcher, now = Date.now()) {
138
175
  }
139
176
  /** Disconnect: Google forgets the grant (best effort), and the file goes. */
140
177
  export async function disconnectGoogle(dir, fetcher) {
141
- const saved = readGoogleMail(dir);
178
+ let saved;
179
+ try {
180
+ saved = readGoogleMail(dir);
181
+ }
182
+ catch {
183
+ saved = null;
184
+ }
142
185
  if (saved?.refreshToken) {
143
186
  try {
144
187
  await fetcher(`${GOOGLE_REVOKE}?${new URLSearchParams({ token: saved.refreshToken })}`, { method: "POST", signal: AbortSignal.timeout(10_000) });
@@ -147,5 +190,5 @@ export async function disconnectGoogle(dir, fetcher) {
147
190
  }
148
191
  if (saved !== null)
149
192
  cached.delete(saved.clientId);
150
- rmSync(join(dir, FILE), { force: true });
193
+ writeHeldSecret(dir, HELD.name, HELD.file, null);
151
194
  }
package/dist/html.d.ts CHANGED
@@ -55,6 +55,15 @@ export type PostFormOptions = {
55
55
  };
56
56
  /** The server's request context supplies the request's CSRF token (server/request-context.ts). */
57
57
  export declare function provideFormToken(source: () => string | undefined): void;
58
+ export declare function providePasswordless(source: () => boolean): void;
59
+ /**
60
+ * A ceremony's re-authentication field: rendered as given, or — with Passwordless on for this signed-in browser —
61
+ * nothing at all (the form posts an empty password; the act proves the session). Never used for credential acts,
62
+ * the sign-in form, a new password, or a secret or API key field.
63
+ */
64
+ export declare function reauth(field: Html): Html;
65
+ /** The same question for a page's own script that builds a re-authentication field itself. */
66
+ export declare const passwordlessRendering: () => boolean;
58
67
  /**
59
68
  * A POST form: the action, the request's one CSRF field, the optional
60
69
  * project revision and return fields, other hidden fields, then the body.
package/dist/html.js CHANGED
@@ -111,6 +111,17 @@ export function attributes(values) {
111
111
  let formToken = () => undefined;
112
112
  /** The server's request context supplies the request's CSRF token (server/request-context.ts). */
113
113
  export function provideFormToken(source) { formToken = source; }
114
+ /** Passwordless (docs/plans/passwordless.md), as the request sees it: the request context provides it. */
115
+ let passwordlessNow = () => false;
116
+ export function providePasswordless(source) { passwordlessNow = source; }
117
+ /**
118
+ * A ceremony's re-authentication field: rendered as given, or — with Passwordless on for this signed-in browser —
119
+ * nothing at all (the form posts an empty password; the act proves the session). Never used for credential acts,
120
+ * the sign-in form, a new password, or a secret or API key field.
121
+ */
122
+ export function reauth(field) { return passwordlessNow() ? html `` : field; }
123
+ /** The same question for a page's own script that builds a re-authentication field itself. */
124
+ export const passwordlessRendering = () => passwordlessNow();
114
125
  /**
115
126
  * A POST form: the action, the request's one CSRF field, the optional
116
127
  * project revision and return fields, other hidden fields, then the body.