toolroll 0.9.85 → 0.9.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
@@ -1,40 +1,30 @@
1
1
  /**
2
- * The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
3
- * its promise, its steps in order, what it needs and "Use this". A template's page asks only what it
4
- * needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
2
+ * New flow (Flows → New): describe it to the lead, or start from a template. Templates are rows grouped by what
3
+ * they're for, each with its promise, its steps in order, what it needs and one "Use this"; the starter kits ("With
4
+ * an agent"), the recipes ("One-time work") and the starter flows join them as groups. A template's page asks only
5
+ * what it needs, previews in plain words what it will do and never do, and creates it: the same answers it previewed.
5
6
  * A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
6
7
  */
7
8
  import { brandMarkHtml } from "./brand-mark.js";
8
9
  import { BLANK, GALLERY, GALLERY_GROUPS, galleryAdmitsAnyone, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
9
10
  import { choiceTargets } from "./flows.js";
10
11
  import { html, joinHtml, postForm, reauth } from "./html.js";
11
- export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
12
- `.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
13
- `.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
14
- `.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
15
- // A template's steps in order, joined by arrows; a person's own steps carry the attention chip.
16
- `.gallery-steps{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;font-weight:500;line-height:1.5;color:var(--so-ink)}` +
17
- `.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}` +
18
- `.gallery-steps b{font-weight:500;font-size:11.5px;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-signal-soft);color:var(--so-signal)}` +
19
- `.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
20
- // An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
21
- `.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
22
- `.gallery-blank{margin-top:20px}` +
23
- // A template's tools: the mark Settings → Integrations shows, its name and where the project stands.
24
- `.gallery-tools{display:grid;gap:6px;list-style:none;padding:0;margin:0}.gallery-tools li{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}.gallery-tools li>span:not(.brand-mark){min-width:0;overflow-wrap:anywhere}` +
25
- `.gallery-tools strong{font-weight:500;overflow-wrap:anywhere}.gallery-connect{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.gallery-connect:last-child{border-bottom:1px solid var(--so-line);margin-bottom:4px}` +
26
- `.gallery-connect form{display:grid;gap:8px;margin:0}.gallery-connect p{margin:0;overflow-wrap:anywhere}.gallery-connect .gallery-tool-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.gallery-connect button{justify-self:start;min-height:36px;white-space:nowrap;background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}` +
27
- `.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-steps{margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
28
- `.gallery-use label small{font-weight:400;color:var(--so-muted)}.gallery-use label.gallery-check{display:flex;align-items:flex-start;gap:8px;font-weight:500}.gallery-use label.gallery-check input{margin-top:3px;width:16px;height:16px;min-height:0;flex:none}.gallery-use label.gallery-check small{display:block}` +
29
- `.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
30
- `.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
31
- `.gallery-preview ul{margin:0;padding-left:18px;line-height:1.55}.gallery-preview p{margin:0;overflow-wrap:anywhere}.gallery-preview .gallery-never{font-weight:600}` +
32
- `.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
33
- `.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
34
- `.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
35
- `.gallery-use .gallery-actions button[value=create]:not(.secondary){background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
36
- `@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery-connect button{min-height:44px;justify-self:stretch}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
37
- `.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
12
+ import { acts, btn, chip, composer, count, disclose, doc, docHeader, dock, field, group, infoLine, input as kInput, note, notice, phoneHeader, segLinks, select, sendButton } from "./server/kit.js";
13
+ export const GALLERY_CSS = `.fl-new .k-sec{gap:12px}.fl-th{display:grid;justify-items:start;gap:10px}.fl-th .k-seg{flex-wrap:wrap}.fl-tsel{justify-self:stretch}.fl-tsel form{display:flex;gap:8px;margin:0}.fl-tsel .k-selw{flex:1}` +
14
+ `.fl-tlist{display:grid}.fl-tlist .k-grp{padding:0;margin-top:16px}.fl-tlist .k-grp:first-child{margin-top:0}` +
15
+ `.fl-trow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--so-line)}` +
16
+ `.fl-tmain{display:grid;gap:3px;min-width:0}.fl-tmain>b{font:500 13.5px/1.35 var(--so-font-sans);color:var(--so-ink)}.fl-tp{color:var(--so-muted);font-size:12.5px;line-height:1.45;overflow-wrap:anywhere}` +
17
+ `.fl-tline{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;font-size:12px;margin-top:3px;color:var(--so-muted);min-width:0;overflow-wrap:anywhere}.fl-ta .k-btn{min-width:108px}.fl-ta form{margin:0}` +
18
+ // A template's steps in order, joined by arrows.
19
+ `.gallery-steps{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;line-height:1.5;color:var(--so-ink)}` +
20
+ `.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}.gallery-steps b{font-weight:500}` +
21
+ `.gallery-tools{display:inline-flex;flex-wrap:wrap;gap:4px 10px;list-style:none;padding:0;margin:0}.gallery-tools li{display:inline-flex;align-items:center;gap:6px;min-width:0}` +
22
+ `.fl-blank{padding-top:4px}.fl-describe-note{margin-top:2px}` +
23
+ // A template's page: the tools to connect, the form, and what it will do.
24
+ `.fl-use form{display:grid;gap:18px;margin:0}.fl-tool{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.fl-tool:last-child{border-bottom:1px solid var(--so-line)}.fl-tool form{gap:10px}` +
25
+ `.fl-tool-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;font-size:13px;font-weight:500}.fl-check{display:flex;align-items:flex-start;gap:8px;font-size:13px;font-weight:500;cursor:pointer}.fl-check input{margin-top:3px;width:16px;height:16px;flex:none}.fl-check .k-note{display:block;font-weight:400}` +
26
+ `.fl-will{display:grid;gap:10px;padding:16px 0;border-top:1px solid var(--so-line);border-bottom:1px solid var(--so-line)}.fl-will ul{margin:0;padding-left:18px;font-size:13px;line-height:1.55}.fl-will ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:12.5px;line-height:1.5;color:var(--so-muted)}.fl-will li{overflow-wrap:anywhere}.fl-never{margin:0;font-size:13px;font-weight:600}` +
27
+ `@media(max-width:760px){.fl-page .k-btn.k-docked,.fl-new .k-btn.k-docked,.fl-use .k-btn.k-docked,.fl-kit .k-btn.k-docked{display:none}.fl-trow{grid-template-columns:minmax(0,1fr);gap:10px;padding:14px 0}.fl-ta .k-btn{width:100%}.fl-ta:empty{display:none}.fl-check input{width:20px;height:20px}}`;
38
28
  /** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
39
29
  * first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
40
30
  export function mainPath(definition) {
@@ -51,32 +41,64 @@ export function mainPath(definition) {
51
41
  }
52
42
  /** A step's short name in the strip: a person's own steps from their side. */
53
43
  const stepName = (stage) => stage.kind === "choose" ? "You choose" : stage.kind === "approval" ? "You approve" : stage.kind === "send" ? "Sent to you" : stage.title;
54
- /** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
44
+ /** A flow's steps in order, joined by arrows. */
55
45
  export function stepStrip(definition) {
56
46
  return html `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
57
47
  const name = stepName(one);
58
- return html `<li>${i === 0 ? "" : html `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? html `<b data-person>${name}</b>` : name}</li>`;
48
+ return html `<li>${i === 0 ? "" : html `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? html `<b>${name}</b>` : name}</li>`;
59
49
  })}</ol>`;
60
50
  }
61
- const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : html `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
62
- : html `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => html `<li>${one}</li>`)}</ul>`;
63
- /** A tool's state in words, as Settings → Integrations badges it; nothing without a project. */
64
- const STATE = { connected: ["connected", "Connected"], open: ["not-set-up", "Not connected"], taken: ["not-set-up", "Added another way"] };
65
- const toolState = (tool) => tool.state === null ? "" : html `<span class="integration-state integration-state--${STATE[tool.state][0]}"><i aria-hidden="true"></i>${STATE[tool.state][1]}</span>`;
51
+ /** What a template needs, in words: its needs, else its tools, else nothing to connect. */
52
+ const needsWords = (template) => template.needs.length > 0 ? `Needs ${template.needs.join(", ")}` : (template.tools ?? []).length > 0 ? "" : "Nothing to connect";
53
+ /** A tool's state in words, as Settings → Integrations says it; nothing without a project. */
54
+ const STATE = { connected: "Connected", open: "Not connected", taken: "Added another way" };
55
+ const toolState = (tool) => tool.state === null ? "" : chip(STATE[tool.state], tool.state === "connected" ? "done" : "", { dot: tool.state === "connected" });
66
56
  const toolMark = (tool) => brandMarkHtml(tool.id.replace(/-desktop$/, ""), tool.label, tool.state === "connected");
67
57
  const toolsList = (tools) => tools.length === 0 ? ""
68
- : html `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => html `<li data-tool="${one.id}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<strong>${one.label}</strong>${toolState(one)}</li>`)}</ul>`;
69
- /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
58
+ : html `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => html `<li data-tool="${one.id}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<span>${one.label}${one.state === "open" ? " (not connected)" : ""}</span></li>`)}</ul>`;
59
+ /** One row of New flow: name · promise · one line · one outline action (the same shape for templates, kits, recipes and starters). */
60
+ export function templateRow(options) {
61
+ return html `<div class="fl-trow"${options.attrs ?? ""}><div class="fl-tmain"><b>${options.name}</b><span class="fl-tp">${options.promise}</span>${options.line ? html `<span class="fl-tline">${options.line}</span>` : ""}</div><div class="fl-ta">${options.action ?? ""}</div></div>`;
62
+ }
63
+ /** The groups New flow filters by: the template groups, then kits, recipes and starter flows. */
64
+ export const NEW_FLOW_GROUPS = [["all", "All"], ...GALLERY_GROUPS.map(one => [one.id, one.label]), ["kits", "With an agent"], ["recipes", "One-time work"], ["starters", "Starter flows"]];
65
+ /** The templates as rows, grouped (or one group): each with one "Use this". `repo` carries a chosen project to the template's page. */
70
66
  export function galleryHtml(input) {
71
67
  const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
72
- const card = (template) => {
68
+ const rowOf = (template) => {
73
69
  const definition = galleryDiagram(template);
74
- return html `<article class="gallery-card" data-template="${template.id}"><h3>${template.name}</h3><p class="gallery-promise">${template.promise}</p>${stepStrip(definition)}${toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null))}${needsList(template)}${input.canUse && html `<a class="button-link" href="${href(template.id)}" aria-label="Use ${template.name}">Use this</a>`}</article>`;
70
+ const needs = needsWords(template);
71
+ return templateRow({ name: template.name, promise: template.promise, attrs: html ` data-template="${template.id}"`,
72
+ line: html `${stepStrip(definition)}${toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null))}${needs ? html `<span>· ${needs}</span>` : ""}`,
73
+ action: input.canUse ? btn("Use this", { sm: true, href: href(template.id), attrs: { "aria-label": `Use ${template.name}` } }) : "" });
75
74
  };
76
- const groups = GALLERY_GROUPS.map(group => html `<h2 id="gallery-${group.id}">${group.label}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card)}</div>`);
77
- return html `<section class="gallery">${groups}${input.canUse ? html `<p class="gallery-blank meta"><a href="${href(BLANK.id)}">Start from a blank flow</a></p>` : html `<p class="meta">An approver creates flows.</p>`}</section>`;
75
+ const shown = GALLERY_GROUPS.filter(one => input.group === undefined || input.group === "all" || input.group === one.id);
76
+ return html `${shown.map(one => { const templates = GALLERY.filter(template => template.group === one.id); return html `${group(one.label, count(templates.length))}<div data-group="${one.id}">${templates.map(rowOf)}</div>`; })}`;
77
+ }
78
+ /**
79
+ * New flow: describe it (the shared composer; the words go to the lead as a chat draft, the project on its bar), then
80
+ * start from a template, filtered by group. Kits, recipes and starter flows arrive as their own rows.
81
+ */
82
+ export function newFlowHtml(input) {
83
+ const groupKey = NEW_FLOW_GROUPS.some(([key]) => key === input.group) ? input.group : "all";
84
+ const keep = (key) => `/flows/new?${new URLSearchParams({ ...(key === "all" ? {} : { group: key }), ...(input.repo === null ? {} : { repo: input.repo }) }).toString()}`.replace(/\?$/, "");
85
+ const current = input.projects.find(one => one.path === input.repo)?.name ?? input.projects[0]?.name ?? "";
86
+ const describe = input.canUse ? html `<section class="k-sec" aria-labelledby="fl-dh"><h3 id="fl-dh">Describe what you want</h3>\
87
+ <form method="get" action="/flows/new" id="fl-ask-form">${composer({ id: "fl-ask", name: "describe", label: "What should happen", rows: 3, value: input.ask.value, error: input.ask.error,
88
+ placeholder: "Every night, run npm run e2e. When it fails, find the cause, fix it and open a pull request.",
89
+ project: { current, items: input.projects.map(one => one.name), name: "project" }, send: sendButton({ label: "Draft the flow" }),
90
+ note: "Opens in chat: your lead drafts it for you to check. Nothing runs until you create it." })}</form></section>` : "";
91
+ const show = (key) => groupKey === "all" || groupKey === key;
92
+ const filter = html `<span class="k-desk-only">${segLinks("Templates", groupKey, NEW_FLOW_GROUPS.map(([key, label]) => ({ key, label, href: keep(key) })))}</span>\
93
+ <span class="k-phone-only fl-tsel"><form method="get" action="/flows/new">${input.repo === null ? "" : html `<input type="hidden" name="repo" value="${input.repo}">`}${select({ name: "group", label: "Templates", items: NEW_FLOW_GROUPS, current: groupKey, wide: true, attrs: { "data-k-autosubmit": "" } })}<noscript>${btn("Show", { sm: true, type: "submit" })}</noscript></form></span>`;
94
+ const templates = GALLERY_GROUPS.some(one => show(one.id)) ? galleryHtml({ repo: input.repo, canUse: input.canUse, connections: input.connections, group: groupKey }) : "";
95
+ const blank = input.canUse && groupKey === "all" ? html `<p class="k-note fl-blank"><a href="/flows/new/${BLANK.id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}">Start from a blank flow</a></p>` : "";
96
+ const list = html `${templates}${show("kits") ? input.kits : ""}${show("recipes") ? input.recipes : ""}${show("starters") ? input.starters : ""}${blank}`;
97
+ const said = input.problem ? notice(input.problem) : input.said ? infoLine(input.said) : "";
98
+ return html `<div class="fl-new" data-kit-page>${phoneHeader("New flow", { back: "/flows" })}${doc(docHeader({ title: "New flow", back: ["/flows", "Flows"], meta: input.canUse ? "Describe it in a sentence, or start from a template." : "An approver creates flows.", same: true }), said, describe, html `<section class="k-sec" aria-labelledby="fl-th"><div class="fl-th"><h3 id="fl-th">Start from a template</h3>${filter}</div><div class="fl-tlist">${list}</div></section>`)}\
99
+ ${input.canUse ? dock(btn("Draft the flow", { variant: "primary", type: "submit", attrs: { form: "fl-ask-form" } })) : ""}</div>`;
78
100
  }
79
- /** A template's page: its questions, then what it will do and never do, then Create. */
101
+ /** A template's page, a reading page: its steps and tools, its questions, what it will do and never do, then Create. */
80
102
  export function galleryUseHtml(input) {
81
103
  const { template } = input;
82
104
  const tools = input.tools ?? input.preview?.tools ?? [];
@@ -84,32 +106,42 @@ export function galleryUseHtml(input) {
84
106
  const missing = tools.filter(one => one.state !== "connected" && one.state !== "taken");
85
107
  // Each tool with its state; one the project lacks offers its own Connect (the Tools page's, with the same password), and comes back here.
86
108
  const connect = (tool) => {
87
- const head = html `<p class="gallery-tool-head">${toolMark(tool)} <strong>${tool.label}</strong> ${toolState(tool)}</p>`;
109
+ const head = html `<p class="fl-tool-h">${toolMark(tool)} ${tool.label} ${toolState(tool)}</p>`;
88
110
  // Added another way, it is there: nothing to connect.
89
111
  if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
90
- return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
112
+ return html `<div class="fl-tool" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
91
113
  const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
92
- return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
114
+ const fieldId = `fl-connect-${tool.id}`;
115
+ return html `<div class="fl-tool" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `${note(how)}${reauth(field({ id: fieldId, label: "Your Toolroll password", control: kInput({ id: fieldId, name: "password", type: "password", attrs: { autocomplete: "current-password", required: true } }) }))}${acts(btn(`Connect ${tool.label}`, { type: "submit", attrs: { name: "service", value: tool.id } }))}`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
93
116
  };
94
- const toolsBlock = tools.length === 0 ? "" : html `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)}</section>`;
117
+ const toolsBlock = tools.length === 0 ? "" : html `<section class="k-sec" aria-label="Tools"><h3>Tools</h3><div>${tools.map(connect)}</div></section>`;
95
118
  // Made without a tool, the preview names the zone that needs it.
96
119
  const zones = (tool) => joinHtml(tool.zones.map(zone => html `“${zone}”`), " and ");
97
- const needing = html `${missing.filter(one => one.zones.length > 0).map(one => html `<p class="meta" data-needs-tool="${one.id}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${one.label}, which isn't connected.</p>`)}${tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => html `<p class="meta" data-tool-taken="${one.id}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${one.label}, added another way.</p>`)}`;
98
- const field = (ask) => {
99
- const value = input.answers[ask.key] ?? ask.default;
100
- const hint = ask.hint === undefined ? "" : html ` <small>${ask.hint}</small>`;
101
- if (ask.key === "outdated")
102
- return html `<label>${ask.label}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => html `<option value="${id}"${id === value && html ` selected`}>${one.label}</option>`)}</select></label>`;
103
- return html `<label>${ask.label}${hint}<input name="${ask.key}" value="${value}" required maxlength="${ask.key === "command" ? 500 : ask.key === "team" ? 12 : 100}"${ask.key === "command" ? html ` spellcheck="false" autocapitalize="off"` : ask.key === "team" ? html ` spellcheck="false" autocapitalize="characters"` : ""}></label>`;
120
+ const needing = html `${missing.filter(one => one.zones.length > 0).map(one => html `<p class="k-note" data-needs-tool="${one.id}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${one.label}, which isn't connected.</p>`)}${tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => html `<p class="k-note" data-tool-taken="${one.id}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${one.label}, added another way.</p>`)}`;
121
+ const ask = (one, index) => {
122
+ const value = input.answers[one.key] ?? one.default;
123
+ const id = `fl-ask-${index}`;
124
+ if (one.key === "outdated")
125
+ return field({ id, label: one.label, control: select({ id, name: "outdated", items: Object.entries(OUTDATED_COMMANDS).map(([key, command]) => [key, command.label]), current: value, wide: true }) });
126
+ return field({ id, label: one.label, ...(one.hint === undefined ? {} : { hint: one.hint }), control: kInput({ id, name: one.key, value, hint: one.hint !== undefined,
127
+ attrs: { required: true, maxlength: one.key === "command" ? 500 : one.key === "team" ? 12 : 100, ...(one.key === "command" ? { spellcheck: "false", autocapitalize: "off" } : one.key === "team" ? { spellcheck: "false", autocapitalize: "characters" } : {}) } }) });
104
128
  };
105
129
  const project = input.projects.length > 1
106
- ? html `<label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label>`
130
+ ? field({ id: "fl-use-project", label: "Project", control: select({ id: "fl-use-project", name: "repo", items: input.projects.map(one => [one.path, one.name]), current: input.repo, wide: true }) })
107
131
  : html `<input type="hidden" name="repo" value="${input.repo}">`;
108
- const preview = input.preview === null ? "" : html `<section class="gallery-preview" aria-labelledby="gallery-will"><h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="gallery-never">${input.preview.built.never}</p>${needing}${input.preview.startsFrom.length === 0 ? html `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => html `<p class="meta">Starts from: ${one}</p>`)}<details><summary>Every step</summary><ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol></details></section>`;
109
- const sendResult = !sendsAlready(template) && html `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`;
110
- // A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3).
111
- const step = input.preview !== null && galleryAdmitsAnyone(template) ? reauth(html `<label>Your password <span class="meta">(it brings in items from anyone)</span><input type="password" name="password" autocomplete="current-password"></label>`) : "";
112
- const actions = html `${step}<p class="gallery-actions">${input.preview !== null && html `<button type="submit" name="intent" value="create"${missing.length > 0 && html ` class="secondary" data-without-tools`}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>`;
113
- const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(field)}<label>Name<input name="name" value="${input.name}" maxlength="80"></label>${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true }, hidden: { previewed: input.preview?.digest ?? "" } });
114
- return html `<section class="gallery-use" data-gallery-template="${template.id}">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}${input.said ? html `<p role="status">${input.said}</p>` : ""}<p>${template.promise}</p>${input.diagram !== null && stepStrip(input.diagram)}${needsList(template)}${toolsBlock}${form}<p class="meta"><a href="/flows/new">All templates</a></p></section>`;
132
+ const preview = input.preview === null ? "" : html `<section class="k-sec fl-will" aria-labelledby="fl-will"><h3 id="fl-will">What it will do</h3><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="fl-never">${input.preview.built.never}</p>${needing}\
133
+ ${input.preview.startsFrom.length === 0 ? note("Cards start when you add them.") : input.preview.startsFrom.map(one => note(`Starts from: ${one}`))}${disclose("Every step", html `<ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol>`)}</section>`;
134
+ const sendResult = !sendsAlready(template) && html `<label class="fl-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<span class="k-note">When a card finishes, what was done comes to you in your chat apps.</span></span></label>`;
135
+ // A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3): a step-up, which Passwordless stands in for.
136
+ const anyone = input.preview !== null && galleryAdmitsAnyone(template);
137
+ const step = anyone ? reauth(field({ id: "fl-use-password", label: "Your password", hint: "It brings in items from anyone.", control: kInput({ id: "fl-use-password", name: "password", type: "password", hint: true, attrs: { autocomplete: "current-password" } }) })) : "";
138
+ const creates = input.preview !== null;
139
+ const createButton = (docked) => btn("Create flow", { variant: missing.length > 0 ? "outline" : "primary", type: "submit", ...(docked ? { docked: true } : {}),
140
+ attrs: { name: "intent", value: "create", ...(missing.length > 0 ? { "data-without-tools": "" } : {}) } });
141
+ const actions = html `${step}${acts(creates ? createButton(true) : "", btn(creates ? "Update preview" : "Preview", { variant: creates ? "outline" : "primary", type: "submit", attrs: { name: "intent", value: "preview" } }))}${creates ? note(anyone ? "Creating it asks for your password: it brings in items from anyone." : "Creating it needs no password.") : ""}`;
142
+ const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(ask)}${field({ id: "fl-use-name", label: "Name", control: kInput({ id: "fl-use-name", name: "name", value: input.name, attrs: { maxlength: 80 } }) })}${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true, id: "fl-use-form" }, hidden: { previewed: input.preview?.digest ?? "" } });
143
+ const needs = needsWords(template);
144
+ const steps = input.diagram === null && !needs ? "" : html `<section class="k-sec" aria-label="Steps">${input.diagram !== null && stepStrip(input.diagram)}${needs ? note(needs) : ""}</section>`;
145
+ return html `<div class="fl-use" data-kit-page data-gallery-template="${template.id}">${phoneHeader(template.name, { back: "/flows/new" })}${doc(docHeader({ title: template.name, back: ["/flows/new", "New flow"], meta: template.promise, same: true }), input.problem !== null ? notice(input.problem) : "", input.said ? infoLine(input.said) : "", steps, toolsBlock, form)}\
146
+ ${creates ? dock(btn("Create flow", { variant: "primary", type: "submit", attrs: { form: "fl-use-form", name: "intent", value: "create" } })) : dock(btn("Preview", { variant: "primary", type: "submit", attrs: { form: "fl-use-form", name: "intent", value: "preview" } }))}</div>`;
115
147
  }
package/dist/flow-live.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * can't keep one open falls back to reading every few seconds. With nothing
17
17
  * written, an open flow costs no reads; a 30-second check heals a miss.
18
18
  */
19
+ import { browserViewOf } from "./flow-browser.js";
19
20
  import { createHash } from "node:crypto";
20
21
  import { createLiveRooms } from "./live-bus.js";
21
22
  // Statements belong to their database, without keeping closed Stores alive.
@@ -90,7 +91,14 @@ export function flowFingerprint(store, flow, now = new Date()) {
90
91
  // including a build in another project. The page read admits the viewer;
91
92
  // this stream only carries a hash. No evidence files or logs are read.
92
93
  const row = fingerprintStatement(store).get({ $flow: flow, $now: now.toISOString() });
93
- return row === undefined ? null : createHash("sha256").update(Object.values(row).map(String).join("|")).digest("hex").slice(0, 16);
94
+ if (row === undefined)
95
+ return null;
96
+ // Browser use (flow-browser.ts): an approval, a revocation, a changed covered script or a signer who lost standing
97
+ // changes what the page says, so it is part of what open pages are nudged on.
98
+ const saved = store.getFlow(flow);
99
+ const browser = saved === null ? null : browserViewOf(store, saved);
100
+ const browserKey = browser === null ? "" : `${browser.status}:${browser.digest ?? ""}:${browser.signedAt ?? ""}:${browser.problem === null ? "" : "x"}`;
101
+ return createHash("sha256").update([...Object.values(row).map(String), browserKey].join("|")).digest("hex").slice(0, 16);
94
102
  }
95
103
  /** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
96
104
  export function createFlowRooms(fingerprint, options = {}) {
@@ -19,6 +19,7 @@ import { adoptPublishedPullRequest } from "./publish.js";
19
19
  import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
20
20
  import { requestResultChanges } from "./result-actions.js";
21
21
  import { revisionSourceOf } from "./result-review.js";
22
+ import { cardTaskArchived } from "./task-archive.js";
22
23
  /** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
23
24
  * The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
24
25
  * last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
@@ -41,19 +42,23 @@ export function approvedSinceBuild(store, definition, card, builtAt = null) {
41
42
  /** One pass of a Pull request zone for one card. Returns whether it changed anything. */
42
43
  export async function pullRequestStep(store, flow, definition, stage, card, now, io) {
43
44
  const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
44
- const waiting = (said) => {
45
+ // Every write after GitHub answers re-proves, in its own transaction, that the card's work wasn't put away meanwhile
46
+ // (v145): a put away that committed first leaves the card where it is, saying nothing to anyone.
47
+ const unlessHeld = (write) => store.transact(() => cardTaskArchived(store, card.id) ? false : write());
48
+ const waiting = (said) => unlessHeld(() => {
45
49
  if (card.waiting === said)
46
50
  return false;
47
51
  store.updateFlowCard(card.id, { waiting: said }, now);
48
52
  return true;
49
- };
50
- const passed = (said, output) => {
53
+ });
54
+ const passed = (said, output) => unlessHeld(() => {
51
55
  store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: output }, waiting: null }, now);
52
56
  if (stage.next === null)
53
57
  return waiting(`${said} Move the card on when you're ready.`);
54
58
  return store.moveFlowCard(card.id, { to: stage.next, outcome: "ok", actor: "flow", historyNote: said, expectEntry: card.entry }, now);
55
- };
56
- const failed = (said, revision = null) => {
59
+ });
60
+ const failed = (said, revision = null) => unlessHeld(() => failedNow(said, revision));
61
+ const failedNow = (said, revision = null) => {
57
62
  store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: said } }, now);
58
63
  if (stage.onFail === null) {
59
64
  if (!waiting(`${said} Fix it, then move the card to try again.`))
@@ -68,13 +73,13 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
68
73
  };
69
74
  // Publishing that can't proceed is not the build's fault: rebuilding wouldn't change it, so the card waits here,
70
75
  // saying what to do, and the people on it hear once. It carries on by itself once the cause is fixed.
71
- const stuck = (said) => {
76
+ const stuck = (said) => unlessHeld(() => {
72
77
  if (!waiting(said))
73
78
  return false;
74
79
  const people = card.owner === null ? cardFollowers(store, card) : [card.owner];
75
80
  notifyPeople(store, card, people, null, { key: `pr-stuck:${card.entry}`, subject: `${stage.title} is waiting on you for “${card.title}”`, attention: true, body: said }, now);
76
81
  return true;
77
- };
82
+ });
78
83
  // With no failure path, a card that didn't pass waits for a person to move it (a move is a fresh visit).
79
84
  if (stage.onFail === null && card.waiting !== null && card.waiting.endsWith(" Fix it, then move the card to try again."))
80
85
  return false;
@@ -128,6 +133,9 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
128
133
  // Back to a Build zone: a revision of the same work, carrying the failure (fenced as untrusted CI output).
129
134
  let revision = null;
130
135
  const target = definition.stages.find(one => one.id === stage.onFail);
136
+ // No revision of work put away while GitHub answered (v145).
137
+ if (cardTaskArchived(store, card.id))
138
+ return false;
131
139
  if (target?.kind === "task" && io.evidenceRoot !== undefined) {
132
140
  const runTask = store.externalIdFor(store.getRun(runId)?.taskRef ?? -1);
133
141
  const revised = runTask === null ? null : requestResultChanges(store, io.evidenceRoot, {
@@ -54,6 +54,8 @@ function stepOf(stage, ask) {
54
54
  step["runIn"] = stage.runIn;
55
55
  if (stage.secrets !== undefined)
56
56
  step["secrets"] = stage.secrets;
57
+ if (stage.browser === true)
58
+ step["browser"] = true;
57
59
  }
58
60
  if (stage.routes !== undefined)
59
61
  step["routes"] = stage.routes.map(one => ({ answer: one.answer, goesTo: one.to }));
@@ -299,7 +301,7 @@ function stageInputOf(step, index, find) {
299
301
  ...(toOwner ? { toOwner: true } : { approver: anyone ? null : decider }),
300
302
  message: step["message"], close: step["close"], script: step["script"], runIn: step["runIn"],
301
303
  routes: list("routes")?.map((one, n) => ({ answer: one["answer"], to: find(one["goesTo"], `${at}.routes[${n}].goesTo`) })),
302
- secrets: step["secrets"],
304
+ secrets: step["secrets"], browser: step["browser"],
303
305
  ...(kind === "sort" ? { sort: { question: step["question"], answers: (list("answers") ?? []).map((one, n) => ({ answer: one["answer"], means: one["means"], to: find(one["goesTo"], `${at}.answers[${n}].goesTo`) })),
304
306
  sureAt: typeof sureAt === "number" && sureAt > 1 ? sureAt / 100 : sureAt, notes: list("alsoNote") ?? [] } } : {}),
305
307
  ...(kind === "request" ? { request: { method: step["method"], url: step["url"], headers: object("headers") ?? {}, body: step["body"] } } : {}),
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Settings → Flows: the starter flows for a project, each switched on with one yes. A row says what it's for,
3
- * what it will do and what it never does — the whole of what the yes agrees to — then one button. A starter
2
+ * Starter flows (New flow → Starter flows): a project's starter flows, each switched on with one yes. A row says what
3
+ * it's for, what it will do and what it never does (the whole of what the yes agrees to), then one button. A starter
4
4
  * that's on links to its flow; one that can't run here says why.
5
5
  */
6
6
  import type { StarterView } from "./flow-starters.js";
@@ -8,14 +8,8 @@ import { type Html } from "./html.js";
8
8
  export declare const STARTERS_CSS: string;
9
9
  export declare function startersHtml(input: {
10
10
  repo: string;
11
- projects: readonly {
12
- path: string;
13
- name: string;
14
- }[];
11
+ projectName: string;
15
12
  starters: readonly StarterView[];
16
- csrf: string;
17
13
  canSwitch: boolean;
18
14
  suggested: string | null;
19
- said: string | null;
20
- problem: string | null;
21
15
  }): Html;
@@ -1,32 +1,21 @@
1
1
  import { html, postForm } from "./html.js";
2
- export const STARTERS_CSS = `.starters{max-width:760px;min-width:0}.starters h2{margin:24px 0 4px;font-size:.9375rem}` +
3
- `.starter{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:14px 0;border-top:1px solid var(--so-line);min-width:0}` +
4
- `.starter:first-of-type{border-top:0}.starter>:not(.starter-action){grid-column:1}.starter h3{margin:0;font-size:.9375rem;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}` +
5
- `.starter p,.starter ul{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}.starter ul{padding-left:18px;color:var(--so-muted);font-size:.875rem;line-height:1.5}` +
6
- `.starter .starter-never{font-size:.875rem;font-weight:500}.starter-action{grid-column:2;grid-row:1 / span 4;align-self:center;margin:0}` +
7
- `.starter-action button,.starter-action .button-link{min-height:44px;white-space:nowrap}.starter .starter-action button{width:auto}` +
8
- `.starter .starter-action a.starter-open{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
9
- `.starter[data-suggested="true"]{margin:0 -12px;padding:14px 12px;border:1px solid var(--so-accent,var(--so-line));border-radius:10px}` +
10
- `.starter-on{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:500;padding:0 8px;border-radius:5px;line-height:20px;color:var(--so-success);background:var(--so-success-soft)}` +
11
- `.starter-on i{width:6px;height:6px;border-radius:50%;background:currentColor}.starters .starter-blocked{color:var(--so-muted);font-size:.875rem}` +
12
- `.starters form.starters-project{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:0 0 8px}.starters form.starters-project label{display:grid;gap:4px}` +
13
- `@media (max-width:560px){.starter{grid-template-columns:minmax(0,1fr)}.starter .starter-action{grid-column:1;grid-row:auto;margin-top:10px}` +
14
- `.starter .starter-action button,.starter .starter-action .button-link{width:100%;justify-content:center}.starters select{font-size:16px}}`;
2
+ import { templateRow } from "./flow-gallery-ui.js";
3
+ import { btn, chip, group, note } from "./server/kit.js";
4
+ export const STARTERS_CSS = `.fl-starter-does{margin:2px 0 0;padding-left:18px;font-size:12.5px;line-height:1.5;color:var(--so-muted)}.fl-starter-never{font-weight:500;color:var(--so-ink)}` +
5
+ `.fl-trow[data-suggested="true"]{border-left:2px solid var(--so-ink);padding-left:12px}`;
15
6
  export function startersHtml(input) {
16
- const note = input.problem ? html `<p class="problem" role="alert">${input.problem}</p>` : input.said ? html `<p role="status">${input.said}</p>` : "";
17
- const selector = input.projects.length > 1
18
- ? html `<form class="starters-project" method="get" action="/settings/flows"><label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label><button>Show</button></form>` : "";
19
7
  // The one asked about ("Do this every time…") comes first, marked.
20
8
  const ordered = [...input.starters].sort((a, b) => Number(b.id === input.suggested) - Number(a.id === input.suggested));
21
9
  const rows = ordered.map(one => {
22
- const action = one.on !== null ? html `<p class="starter-action"><a class="button-link starter-open" href="/flows/${one.on.flow}">Open flow</a></p>`
10
+ const action = one.on !== null ? btn("Open flow", { sm: true, href: `/flows/${one.on.flow}` })
23
11
  : one.blocked !== null || !input.canSwitch ? ""
24
- : postForm("/settings/flows/on", html `<button type="submit">Switch on</button>`, { attrs: { class: "starter-action" }, hidden: { repo: input.repo, starter: one.id } });
12
+ : postForm("/settings/flows/on", btn("Switch on", { sm: true, type: "submit", attrs: { "aria-label": `Switch on ${one.name}` } }), { hidden: { repo: input.repo, starter: one.id } });
25
13
  // What the yes agrees to, in full, before it; nothing to agree to once it's on or can't be.
26
- const agrees = one.on === null && one.blocked === null && html `<ul>${one.does.map(line => html `<li>${line}</li>`)}</ul><p class="starter-never">${one.never}</p>`;
27
- const head = html `<h3>${one.name}${one.on === null ? "" : html ` <span class="starter-on"><i aria-hidden="true"></i>On</span>`}</h3>`;
28
- return html `<div class="starter" id="starter-${one.id}" data-starter="${one.id}" data-suggested="${String(one.id === input.suggested && one.on === null)}">${head}<p>${one.summary}</p>${agrees}${one.on === null && one.blocked !== null && html `<p class="starter-blocked">${one.blocked}</p>`}${action}</div>`;
14
+ const line = one.on !== null ? chip("On", "done")
15
+ : one.blocked !== null ? html `<span>${one.blocked}</span>`
16
+ : html `<span><ul class="fl-starter-does">${one.does.map(words => html `<li>${words}</li>`)}</ul><span class="fl-starter-never">${one.never}</span></span>`;
17
+ return templateRow({ name: one.name, promise: one.summary, line, action, attrs: html ` id="starter-${one.id}" data-starter="${one.id}" data-suggested="${String(one.id === input.suggested && one.on === null)}"` });
29
18
  });
30
- const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? html `<p class="meta">An approver switches these on.</p>` : "";
31
- return html `<section class="starters">${note}${selector}<h2>Starter flows</h2>${rows}${nobody}<p class="meta"><a href="/flows">All flows</a></p></section>`;
19
+ const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? note("An approver switches these on.") : "";
20
+ return html `${group(`Starter flows for ${input.projectName}`)}<div data-group="starters">${rows}</div>${nobody}`;
32
21
  }
@@ -1,7 +1,7 @@
1
1
  import type { Runner } from "./backend.js";
2
2
  import { type DraftRunner } from "./flow-draft.js";
3
3
  import { type MailSender, type ToolCaller } from "./flow-actions.js";
4
- import type { Store } from "./store.js";
4
+ import type { FlowCardRow, FlowStepKind, Store } from "./store.js";
5
5
  import type { TurnRunner } from "./subagents.js";
6
6
  import type { ToolLister } from "./subagent-tools.js";
7
7
  export type StepIo = {
@@ -42,3 +42,11 @@ export type StepPass = {
42
42
  };
43
43
  /** Run every due check and update in one project, then move each card on. */
44
44
  export declare function runFlowSteps(store: Store, repo: string, now: Date, io: StepIo): Promise<StepPass>;
45
+ /** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
46
+ * before another pass (or a person) moved it never runs the step for a visit that's over. */
47
+ export declare function claimStep(store: Store, card: FlowCardRow, step: {
48
+ stage: string;
49
+ kind: FlowStepKind;
50
+ script: string | null;
51
+ scriptVersion: number | null;
52
+ }, now: Date): boolean;
@@ -36,7 +36,9 @@ import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft
36
36
  import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
37
37
  import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
38
38
  import { cleanFolder, flowLaunch, runCode } from "./flow-code.js";
39
+ import { browserStanding } from "./flow-browser.js";
39
40
  import { readProviderKey } from "./keys.js";
41
+ import { cardTaskArchived } from "./task-archive.js";
40
42
  import { replyInChannel } from "./chat-inbox.js";
41
43
  import { recordSent, threadOf } from "./flow-replies.js";
42
44
  import { ASKED, subagentReady, subagentTurn } from "./subagent-work.js";
@@ -70,6 +72,9 @@ export async function runFlowSteps(store, repo, now, io) {
70
72
  const stage = known.definition?.stages.find(one => one.id === card.stage);
71
73
  if (stage === undefined)
72
74
  continue;
75
+ // A card whose task is put away (v145) runs nothing — no step, subagent turn or pull request — until it's restored.
76
+ if (cardTaskArchived(store, card.id))
77
+ continue;
73
78
  // v92: a subagent's turn — a zone it handles, or a decision it staffs (a person decides when it's paused or gone).
74
79
  if (stage.kind === "subagent" || (stage.kind === "approval" && stage.subagent !== undefined)) {
75
80
  await subagentStep(store, known.flow, known.definition, stage, card, now, io, pass);
@@ -145,6 +150,14 @@ export async function runFlowSteps(store, repo, now, io) {
145
150
  store.updateFlowCard(card.id, { waiting }, now);
146
151
  continue;
147
152
  }
153
+ // A script that starts a browser waits, saying so, until a person approves browser use for the flow (flow-browser.ts),
154
+ // rather than run and crash inside the sandbox; it runs on its own once approved.
155
+ const browser = stage.kind === "check" && stage.browser === true ? (script === null || known.definition === null ? { allowed: false, said: "This zone's script can't be read." } : browserStanding(store, known.flow.id, stage.id, { definition: known.definition, script })) : null;
156
+ if (browser !== null && !browser.allowed) {
157
+ if (card.waiting !== browser.said)
158
+ store.updateFlowCard(card.id, { waiting: browser.said }, now);
159
+ continue;
160
+ }
148
161
  if (!claimStep(store, card, { stage: stage.id, kind: stage.kind, script: script?.name ?? null, scriptVersion: script?.version ?? null }, now))
149
162
  continue;
150
163
  pass.ran++;
@@ -174,12 +187,21 @@ export async function runFlowSteps(store, repo, now, io) {
174
187
  }
175
188
  /** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
176
189
  * before another pass (or a person) moved it never runs the step for a visit that's over. */
177
- function claimStep(store, card, step, now) {
190
+ export function claimStep(store, card, step, now) {
178
191
  return store.transact(() => {
179
192
  const fresh = store.getFlowCard(card.id);
180
193
  if (fresh === null || fresh.state !== "active" || fresh.entry !== card.entry || fresh.stage !== step.stage)
181
194
  return false;
182
- return store.claimFlowStep({ card: card.id, entry: card.entry, ...step }, now);
195
+ // A card whose task is put away (v145) runs no step until the task is restored (both of its tasks, re-read here).
196
+ if (cardTaskArchived(store, card.id))
197
+ return false;
198
+ if (!store.claimFlowStep({ card: card.id, entry: card.entry, ...step }, now))
199
+ return false;
200
+ // The tasks this step was admitted for stay held while it runs, wherever the card moves meanwhile.
201
+ for (const task of [fresh.task, fresh.primaryTask])
202
+ if (task !== null)
203
+ store.handle.prepare("INSERT OR IGNORE INTO flow_step_task (card, entry, task_id) VALUES (?, ?, ?)").run(card.id, card.entry, task);
204
+ return true;
183
205
  });
184
206
  }
185
207
  /** One subagent turn, when one is due for this card: claimed like any step, retried on failure, and waiting while its question is open. */
@@ -361,16 +383,31 @@ async function runCheck(store, flow, stage, script, card, now, io) {
361
383
  if (added.code !== 0)
362
384
  return { state: "retry", said: `Couldn't make a copy of${where}: ${added.stderr.trim().split("\n")[0]?.slice(0, 160) ?? "git refused"}.` };
363
385
  }
364
- const launch = flowLaunch(store, { repo: flow.repo, folder: path, scratch: io.scratch, copy, policy: store.flowFencePolicy(flow.id), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
386
+ // Browser use, proved at the moment of launch: a zone that says it starts a browser runs only with a person's standing
387
+ // approval (flow-browser.ts); then its script, and only its script, gets Chromium's one service.
388
+ const wantsBrowser = stage.browser === true;
389
+ // Read once: the drawing and the script this launch runs are the very objects the browser decision is about.
390
+ const running = flowDefinitionOf(flow);
391
+ const standing = !wantsBrowser ? null : running === null || running.stages.find(one => one.id === stage.id) === undefined || JSON.stringify(running.stages.find(one => one.id === stage.id)) !== JSON.stringify(stage)
392
+ ? { allowed: false, said: "This zone changed while it was starting, so it didn't run." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
393
+ if (standing !== null && !standing.allowed) {
394
+ if (copy)
395
+ await io.git("git", ["-C", flow.repo, "worktree", "remove", "--force", path], { timeoutMs: 60_000 }).catch(() => undefined);
396
+ rmSync(path, { recursive: true, force: true });
397
+ return { state: "failed", said: standing.said };
398
+ }
399
+ const launch = flowLaunch(store, { repo: flow.repo, folder: path, scratch: io.scratch, copy, policy: store.flowFencePolicy(flow.id), ...(standing?.allowed === true ? { browser: true } : {}), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
365
400
  try {
366
401
  if (launch !== null && "problem" in launch)
367
402
  return { state: "failed", said: launch.problem };
403
+ // The project's setup never gets the browser rule: only the zone's script asked for it.
404
+ const setupFence = launch?.fence === undefined ? undefined : { ...launch.fence, fenceGrants: withoutBrowser(launch.fence.fenceGrants) };
368
405
  let log = "";
369
406
  const setup = copy ? store.liveWorktreeSetup(flow.repo) : null;
370
407
  if (setup !== null) {
371
408
  const shell = approvedCommandShell(setup.command);
372
409
  const prepared = await io.shell(shell.file, shell.args, { cwd: path, timeoutMs: setup.timeoutMs, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
373
- ...(launch === null ? {} : { env: launch.env, ...(launch.fence ?? {}) }) });
410
+ ...(launch === null ? {} : { env: launch.env, ...(setupFence ?? {}) }) });
374
411
  log += `$ ${setup.command}\n${prepared.stdout}${prepared.stderr}\n`;
375
412
  if (prepared.code !== 0)
376
413
  return { state: "failed", said: `The project's setup failed before ${script.name} ran (exit ${prepared.code}).\n${tail(`${prepared.stdout}\n${prepared.stderr}`)}`, log, exitCode: prepared.code };
@@ -385,7 +422,15 @@ async function runCheck(store, flow, stage, script, card, now, io) {
385
422
  answers: (stage.routes ?? []).map(one => one.answer),
386
423
  };
387
424
  const env = { FLOW_NAME: flow.name, FLOW_CARD_ID: String(card.id), FLOW_CARD_TITLE: card.title, FLOW_COMMIT: commit ?? "", FLOW_SCRIPT: script.name, FLOW_PROJECT: flow.repo };
388
- const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell, launch });
425
+ // Checked again right before the script: the flow or its script may have changed while the setup ran.
426
+ if (wantsBrowser) {
427
+ const still = running === null ? { allowed: false, said: "This flow's drawing can't be read." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
428
+ if (!still.allowed)
429
+ return { state: "failed", said: still.said, log };
430
+ }
431
+ // And at every spawn attempt (a retry after a failed spawn too): revoked or changed meanwhile, nothing starts.
432
+ const stillApproved = !wantsBrowser || running === null ? undefined : () => browserStanding(store, flow.id, stage.id, { definition: running, script }).allowed;
433
+ const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell, launch, ...(stillApproved === undefined ? {} : { beforeSpawn: stillApproved }) });
389
434
  log += ran.log;
390
435
  const output = ran.output === "" ? undefined : ran.output;
391
436
  // Where it ran goes at the end of the first line: "run-tests failed (exit 1) on main."
@@ -407,6 +452,11 @@ async function runCheck(store, flow, stage, script, card, now, io) {
407
452
  rmSync(path, { recursive: true, force: true });
408
453
  }
409
454
  }
455
+ /** A fence's grants without Chromium's service (a flow's setup, beside a script zone that has it). */
456
+ function withoutBrowser(grants) {
457
+ const { browser: _scriptOnly, ...rest } = grants;
458
+ return rest;
459
+ }
410
460
  /** An update: a comment on the issue the card came from, and closing it if the zone says so. */
411
461
  async function updateSource(store, stage, card, io, now) {
412
462
  const source = card.source;