toolroll 0.9.85 → 0.9.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
package/dist/kits-ui.js CHANGED
@@ -1,46 +1,42 @@
1
1
  import { kitChecklist, kitInstalled, KITS } from "./kits.js";
2
2
  import { SUBAGENT_TEMPLATES } from "./subagents.js";
3
3
  import { html, postForm, reauth } from "./html.js";
4
- const CHECK = html `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
5
- const RING = html `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>`;
6
- export const KITS_CSS = `.kits{max-width:960px;min-width:0}.kits-lede{color:var(--so-muted);margin:.25rem 0 1.5rem;max-width:40rem}.kit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}` +
7
- `.kit{display:flex;flex-direction:column;gap:10px;padding:20px;border:1px solid var(--so-line);border-radius:12px;background:var(--so-paper)}.kit h2{font-size:1.1rem;margin:0}.kit p{margin:0}.kit .kit-promise{line-height:1.5}` +
8
- `.kit .kit-gets{font-size:.85rem;color:var(--so-muted);line-height:1.5}.kit form{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin-top:auto;padding-top:6px}.kit form label{display:grid;gap:4px;font-size:.85rem;flex:1 1 140px}` +
9
- `.kit button,.kit .kit-open{min-height:44px}.kit .kit-open{display:inline-flex;align-items:center;margin-top:auto;font-weight:600}` +
10
- `.kit-steps{list-style:none;padding:0;margin:1rem 0;display:grid;gap:2px;max-width:44rem}.kit-steps li{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper)}` +
11
- `.kit-steps svg{flex:none;width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}.kit-steps li[data-done="true"] svg{color:var(--so-success)}.kit-steps li[data-done="false"] svg{color:var(--so-muted)}` +
12
- `.kit-steps .kit-step-said{flex:1;min-width:0}.kit-steps li[data-done="true"] .kit-step-said{color:var(--so-muted)}.kit-steps form{margin:0}.kit-steps button,.kit-steps .kit-step-go{min-height:40px;white-space:nowrap}` +
13
- `.kit-steps .kit-step-go{display:inline-flex;align-items:center;padding:0 12px;border:1px solid var(--so-line);border-radius:8px;text-decoration:none;font-size:.9rem}` +
14
- `@media(max-width:600px){.kit-steps li{flex-wrap:wrap}.kit-steps .kit-step-said{flex-basis:calc(100% - 40px)}.kit form select{font-size:16px}}`;
4
+ import { templateRow } from "./flow-gallery-ui.js";
5
+ import { acts, btn, count, doc, docHeader, dock, facts, group, icon, infoLine, input, note, notice, phoneHeader } from "./server/kit.js";
6
+ export const KITS_CSS = `.fl-kit-steps{list-style:none;padding:0;margin:0;display:grid;border-top:1px solid var(--so-line)}` +
7
+ `.fl-kit-steps li{display:flex;align-items:center;gap:12px;min-height:48px;padding:8px 0;border-bottom:1px solid var(--so-line);min-width:0}` +
8
+ `.fl-kit-steps li>.k-i{flex:none;color:var(--so-muted)}.fl-kit-steps li[data-done="true"]>.k-i{color:var(--so-done-ink)}.fl-kit-ring{flex:none;width:14px;height:14px;margin:0 1px;border:1.5px solid var(--so-field-line);border-radius:50%;box-sizing:border-box}` +
9
+ `.fl-kit-said{flex:1;min-width:0;font-size:13px;overflow-wrap:anywhere}.fl-kit-steps li[data-done="true"] .fl-kit-said{color:var(--so-muted)}.fl-kit-steps form{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0}.fl-kit-steps .k-input{width:180px}` +
10
+ `@media(max-width:760px){.fl-kit-steps li{flex-wrap:wrap}.fl-kit-said{flex-basis:calc(100% - 40px)}.fl-kit-steps form,.fl-kit-steps .k-input{width:100%}}`;
15
11
  const subagentWords = (kit) => { const template = SUBAGENT_TEMPLATES.find(one => one.id === kit.subagent.template); return template === undefined ? kit.subagent.handle : template.label.toLowerCase(); };
16
- /** The gallery: every kit, set up in a project with one button (or opened, when it's already there). */
17
- export function kitsGalleryHtml(store, projects, projectName, canSetUp, notice) {
18
- const cards = KITS.map(kit => {
19
- const here = projects.filter(repo => kitInstalled(store, kit, repo) !== null);
20
- const open = projects.length === 1 && here.length === 1;
21
- const name = kit.subagent.handle.charAt(0).toUpperCase() + kit.subagent.handle.slice(1);
22
- const gets = `${name} (${subagentWords(kit)}) · the ${kit.flowName} flow${kit.tools.length === 0 ? "" : ` · works with ${kit.tools.map(one => one.label).join(" and ")}`}`;
23
- const action = open ? html `<a class="kit-open" href="/kits/${kit.id}?repo=${encodeURIComponent(projects[0])}">Open ${kit.name} →</a>`
24
- : !canSetUp || projects.length === 0 ? ""
25
- : postForm(`/kits/${kit.id}/setup`, html `${projects.length === 1 ? ""
26
- : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}${here.includes(repo) ? " (set up)" : ""}</option>`)}</select></label>`}<button>Set it up</button>`, { hidden: { repo: projects.length === 1 ? projects[0] : null } });
27
- return html `<article class="kit" data-kit="${kit.id}"><h2>${kit.name}</h2><p class="kit-promise">${kit.promise}</p><p class="kit-gets">${gets}</p>${action}</article>`;
12
+ const agentName = (kit) => kit.subagent.handle.charAt(0).toUpperCase() + kit.subagent.handle.slice(1);
13
+ const kitHref = (kit, repo) => `/kits/${kit.id}?repo=${encodeURIComponent(repo)}`;
14
+ /** "With an agent": every kit as a row; Set it up (or Open, once it's there) goes to the kit's page for `repo`. */
15
+ export function kitRowsHtml(store, repo) {
16
+ const rows = KITS.map(kit => {
17
+ const here = repo !== null && kitInstalled(store, kit, repo) !== null;
18
+ const gets = `${agentName(kit)} (${subagentWords(kit)}) and the ${kit.flowName} flow${kit.tools.length === 0 ? "" : ` · works with ${kit.tools.map(one => one.label).join(" and ")}`}`;
19
+ return templateRow({ name: kit.name, promise: kit.promise, line: gets, attrs: html ` data-kit="${kit.id}"`,
20
+ action: repo === null ? "" : btn(here ? "Open" : "Set it up", { sm: true, href: kitHref(kit, repo), attrs: { "aria-label": `${here ? "Open" : "Set up"} ${kit.name}` } }) });
28
21
  });
29
- return html `<section class="kits">${notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : ""}<p class="kits-lede">A working setup in one click: a subagent, the flow it works, and what to connect next. Nothing goes out without you approving it.</p><div class="kit-grid">${cards}</div></section>`;
22
+ return html `${group("With an agent", count(KITS.length))}<div data-group="kits">${rows}</div>`;
30
23
  }
31
- /** A kit's own page: what's set up, and the next step on each line. */
32
- export function kitPageHtml(store, kit, repo, dir, canSetUp, notice) {
24
+ /** A kit's own page: what it sets up, Set up (once), then the next step on each line. */
25
+ export function kitPageHtml(store, kit, repo, dir, canSetUp, notice_, projectName = one => one) {
33
26
  const steps = kitChecklist(store, kit, repo, dir);
34
27
  const set = kitInstalled(store, kit, repo);
28
+ const back = `/flows/new?group=kits&repo=${encodeURIComponent(repo)}`;
35
29
  const rows = steps.map(step => {
36
- const go = step.done ? (step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">Open</a>`)
30
+ const go = step.done ? (step.href === null ? "" : btn("Open", { sm: true, href: step.href }))
37
31
  : step.action === "sample" || step.action === "github"
38
- ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, step.action === "sample" ? html `<button>Try it</button>`
32
+ ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, step.action === "sample" ? btn("Try it", { sm: true, type: "submit" })
39
33
  // Issues from anyone come in: it takes your password (an ordinary step-up; a session does under Passwordless).
40
- : html `${reauth(html `<input type="password" name="password" autocomplete="current-password" aria-label="Your password" placeholder="Your password">`)}<button class="secondary">Bring them in</button>`, { hidden: { repo } }) : "")
41
- : step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">${step.action === "connect" ? "Connect" : "Set up"}</a>`;
42
- return html `<li data-step="${step.id}" data-done="${String(step.done)}">${step.done ? CHECK : RING}<span class="kit-step-said">${step.said}</span>${go}</li>`;
34
+ : html `${reauth(input({ id: `fl-kit-${step.id}-password`, name: "password", type: "password", attrs: { autocomplete: "current-password", "aria-label": "Your password", placeholder: "Your password" } }))}${btn("Bring them in", { sm: true, type: "submit" })}`, { hidden: { repo } }) : "")
35
+ : step.href === null ? "" : btn(step.action === "connect" ? "Connect" : "Set up", { sm: true, href: step.href });
36
+ return html `<li data-step="${step.id}" data-done="${String(step.done)}">${step.done ? icon("check", true) : html `<span class="fl-kit-ring" aria-hidden="true"></span>`}<span class="fl-kit-said">${step.said}</span>${go}</li>`;
43
37
  });
44
- const setUp = set === null && canSetUp ? postForm(`/kits/${kit.id}/setup`, html `<button>Set up ${kit.name}</button>`, { hidden: { repo } }) : "";
45
- return html `<section class="kits">${notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : ""}${notice.said ? html `<p class="said" role="status">${notice.said}</p>` : ""}<p class="kits-lede">${kit.promise}</p>${setUp}${rows.length === 0 ? "" : html `<ol class="kit-steps">${rows}</ol>`}<p class="meta"><a href="/kits">All starter kits</a></p></section>`;
38
+ const setUp = set === null && canSetUp ? postForm(`/kits/${kit.id}/setup`, acts(btn(`Set up ${kit.name}`, { variant: "primary", type: "submit", docked: true })), { hidden: { repo }, attrs: { id: "fl-kit-setup" } }) : "";
39
+ const about = facts([["Project", projectName(repo)], ["Agent", `${agentName(kit)} (${subagentWords(kit)})`], ["Flow", kit.flowName], ...(kit.tools.length === 0 ? [] : [["Works with", kit.tools.map(one => one.label).join(" and ")]])]);
40
+ return html `<div class="fl-kit" data-kit-page data-kit="${kit.id}">${phoneHeader(kit.name, { back })}${doc(docHeader({ title: kit.name, back: [back, "New flow"], meta: kit.promise, same: true }), notice_.problem ? notice(notice_.problem) : "", notice_.said ? infoLine(notice_.said) : "", about, set === null ? note("Nothing goes out without you approving it.") : "", setUp, rows.length === 0 ? "" : html `<section class="k-sec" aria-labelledby="fl-kit-next"><h3 id="fl-kit-next">Next steps</h3><ol class="fl-kit-steps">${rows}</ol></section>`)}\
41
+ ${set === null && canSetUp ? dock(btn(`Set up ${kit.name}`, { variant: "primary", type: "submit", attrs: { form: "fl-kit-setup" } })) : ""}</div>`;
46
42
  }
package/dist/kits.js CHANGED
@@ -171,7 +171,7 @@ export function kitChecklist(store, kit, repo, dir) {
171
171
  if (kit.email === true) {
172
172
  // (flow-actions' sendingReady, without its imports: kits load beside the chat actions.)
173
173
  const ready = (googleConnected(dir) !== null || readEmailSettings(dir) !== null) && mailboxReady(dir);
174
- steps.push({ id: "email", done: ready, said: ready ? "Email is connected" : "Connect your email so replies go out and answers come back", href: "/settings#email" });
174
+ steps.push({ id: "email", done: ready, said: ready ? "Email is connected" : "Connect your email so replies go out and answers come back", href: "/settings/alerts#email" });
175
175
  }
176
176
  if (kit.github === true) {
177
177
  const github = githubRepoOf(repo);
@@ -1,5 +1,6 @@
1
1
  import { STALE_KNOWLEDGE } from './project-knowledge.js';
2
- import { html, postForm } from './html.js';
2
+ import { html, joinHtml, postForm } from './html.js';
3
+ import { acts, btn, codeLine, disclose, formRow, input, note, notice, settingsLink, settingsRecord, settingsSection, textarea } from './server/kit.js';
3
4
  // Hidden fields as they always were: a missing value is an empty field.
4
5
  const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? '')]));
5
6
  /** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
@@ -12,18 +13,46 @@ export function knowledgeHtml(view, csrf, canManage, draft, error) {
12
13
  const manage = canManage && !!csrf;
13
14
  const base = { repo: view.repo, identity: view.identity, revision: view.revision };
14
15
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/change', body, { hidden: fields({ ...base, action, ...extra }) });
15
- const referenceFields = (d) => html `<label>Name<input name="title" maxlength="120" value="${d.title}" required></label><label>Reference text<textarea name="content" rows="5" maxlength="12000">${d.content}</textarea></label><details${d.path ? html ` open` : ''}><summary>Use a project document</summary><label>Committed .md or .txt path<input name="path" value="${d.path}" placeholder="docs/design-guide.md"></label><p class="meta">Uses the committed file instead of the text above. Save again to refresh it.</p></details><button type="submit">Save reference</button>`;
16
- const refs = view.knowledge.references.map(ref => html `<details class="card"${draft?.id === ref.id ? html ` open` : ''}><summary><strong>${ref.title}</strong></summary>${ref.path ? html `<span class="meta knowledge-source">${ref.path} · ${ref.sourceRevision?.slice(0, 7)}</span>` : ''}<pre>${ref.content}</pre>${manage ? html `<details${draft?.id === ref.id ? html ` open` : ''}><summary>Edit reference</summary>${form('save', referenceFields(draft?.id === ref.id ? draft : { title: ref.title, content: ref.path ? '' : ref.content, path: ref.path ?? '' }), { id: ref.id })}<details><summary>Remove reference</summary>${form('remove', html `<p>Remove from future tasks. Previous run context stays available.</p><button>Remove reference</button>`, { id: ref.id })}</details></details>` : ''}</details>`);
17
- const history = view.history.map(h => html `<li><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}">Version ${h.revision}</a> · ${h.actor} · ${h.at.slice(0, 16).replace('T', ' ')} UTC</li>`);
16
+ const referenceFields = (d, key) => joinHtml([
17
+ formRow('Name', null, input({ id: `ref-${key}-title`, name: 'title', value: d.title ?? '', attrs: { maxlength: 120, required: true, 'aria-label': 'Name' } }), 'wide'),
18
+ formRow('Reference text', null, textarea({ id: `ref-${key}-content`, name: 'content', rows: 5, value: d.content ?? '', attrs: { maxlength: 12000, 'aria-label': 'Reference text' } }), 'top'),
19
+ disclose('Use a project document', formRow('Committed .md or .txt path', 'Uses the committed file instead of the text above. Save again to refresh it.', input({ id: `ref-${key}-path`, name: 'path', value: d.path ?? '', attrs: { placeholder: 'docs/design-guide.md', 'aria-label': 'Committed .md or .txt path' } }), 'wide'), Boolean(d.path)),
20
+ acts(btn('Save reference', { sm: true, type: 'submit' })),
21
+ ]);
22
+ const refs = view.knowledge.references.map(ref => settingsRecord({
23
+ title: ref.title,
24
+ sub: ref.path ? html `<span class="k-mono">${ref.path} · ${ref.sourceRevision?.slice(0, 7)}</span>` : '',
25
+ more: joinHtml([
26
+ disclose('Read reference', html `<pre class="k-scode">${ref.content}</pre>`, draft?.id === ref.id),
27
+ manage ? disclose('Edit reference', joinHtml([
28
+ form('save', referenceFields(draft?.id === ref.id ? draft : { title: ref.title, content: ref.path ? '' : ref.content, path: ref.path ?? '' }, String(ref.id)), { id: ref.id }),
29
+ disclose('Remove reference', form('remove', joinHtml([note('Remove from future tasks. Previous run context stays available.'), acts(btn('Remove reference', { sm: true, variant: 'danger', type: 'submit' }))]), { id: ref.id })),
30
+ ]), draft?.id === ref.id) : '',
31
+ ]),
32
+ }));
33
+ const history = view.history.map(h => settingsLink({ href: `/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}`, title: `Version ${h.revision}`, status: `${h.actor} · ${h.at.slice(0, 16).replace('T', ' ')} UTC` }));
34
+ const problem = error ? notice(error) : '';
18
35
  if (view.stale) {
19
36
  // One notice, the saved knowledge on demand, one approver action. Editing waits until it is applied.
20
37
  const saved = view.stale.knowledge;
21
- const review = html `<details><summary>Review saved knowledge</summary><pre class="knowledge-instructions">${saved.instructions || 'No project instructions.'}</pre>${saved.references.map(ref => html `<details><summary>${ref.title}</summary><pre>${ref.content}</pre></details>`)}</details>`;
22
- return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}<section class="card knowledge-stale"><p role="status">${STALE_KNOWLEDGE}.</p>${review}${manage ? form('apply', html `<button>Apply saved knowledge</button>`) : ''}</section></section>`;
38
+ const review = disclose('Review saved knowledge', joinHtml([html `<pre class="k-scode">${saved.instructions || 'No project instructions.'}</pre>`, ...saved.references.map(ref => disclose(ref.title, html `<pre class="k-scode">${ref.content}</pre>`))]));
39
+ return joinHtml([problem, settingsSection({ id: 'knowledge-stale', title: 'Saved knowledge', body: joinHtml([
40
+ html `<p class="k-notice info" role="status">${STALE_KNOWLEDGE}.</p>`, review,
41
+ manage ? form('apply', acts(btn('Apply saved knowledge', { variant: 'primary', type: 'submit' }))) : '',
42
+ ]) })]);
23
43
  }
24
- const instructions = html `<pre class="knowledge-instructions">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
25
- const editor = manage ? html `<details class="knowledge-editor"${draft?.instructions !== undefined ? html ` open` : ''}><summary>${view.knowledge.instructions ? 'Edit instructions' : 'Add instructions'}</summary>${form('instructions', html `<textarea aria-label="Project instructions" name="instructions" rows="5" maxlength="4000" placeholder="Goals, preferences, and what good work looks like…">${draft?.instructions ?? view.knowledge.instructions}</textarea><button>Save instructions</button>`)}</details>` : '';
26
- return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${framedSection('Instructions', html `${instructions}${editor}`, 'knowledge-intro')}${framedSection('References', html `${refs.length ? refs : html `<p class="meta">Add a spec, design guide, or useful project document.</p>`}${manage ? html `<details class="card"${draft?.title && !draft.id ? html ` open` : ''}><summary>Add reference</summary>${form('save', referenceFields(draft?.id ? {} : draft ?? {}))}</details>` : ''}`)}${history.length ? html `<details class="knowledge frame"><summary class="frame-head">Change history</summary><div class="frame-panel"><ol>${history}</ol></div></details>` : ''}</section>`;
44
+ const instructions = html `<pre class="k-scode">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
45
+ const editor = manage ? disclose(view.knowledge.instructions ? 'Edit instructions' : 'Add instructions', form('instructions', joinHtml([
46
+ textarea({ id: 'knowledge-instructions', name: 'instructions', rows: 5, value: draft?.instructions ?? view.knowledge.instructions, attrs: { 'aria-label': 'Project instructions', maxlength: 4000, placeholder: 'Goals, preferences, and what good work looks like…' } }),
47
+ acts(btn('Save instructions', { sm: true, type: 'submit' })),
48
+ ])), draft?.instructions !== undefined) : '';
49
+ const add = manage ? disclose('Add reference', form('save', referenceFields(draft?.id ? {} : draft ?? {}, 'new')), Boolean(draft?.title && !draft.id)) : '';
50
+ return joinHtml([
51
+ problem,
52
+ settingsSection({ id: 'instructions', title: 'Instructions', lead: 'What every task in this project reads first.', body: joinHtml([instructions, editor]) }),
53
+ settingsSection({ id: 'references', title: 'References', body: joinHtml([refs.length ? joinHtml(refs) : note('Add a spec, design guide, or useful project document.'), add]) }),
54
+ history.length ? settingsSection({ id: 'knowledge-history', title: 'Change history', body: html `<div class="k-slinks">${history}</div>` }) : '',
55
+ ]);
27
56
  }
28
57
  export function knowledgeContextHtml(snapshot) {
29
58
  if (!snapshot)
@@ -35,19 +64,47 @@ export function decisionsHtml(repo, decisions, csrf, canManage, draft, error) {
35
64
  const manage = canManage && !!csrf;
36
65
  const base = { repo };
37
66
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/decision', body, { hidden: fields({ ...base, action, ...extra }) });
38
- const rows = decisions.map(d => html `<details class="card decision" id="decision-${d.id}"><summary><strong>${d.claim}</strong> <span class="meta">#${d.id} · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}</span></summary><p>${d.why}</p><p class="meta">${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}</p>${manage ? form('retire', html `<label>Why it no longer holds<input name="reason" maxlength="500" required></label><button type="submit">Retire</button>`, { decision: d.id }) : ''}</details>`);
39
- const add = manage ? html `<details class="knowledge-editor"${draft?.claim !== undefined ? html ` open` : ''}><summary>Record a decision</summary>${form('record', html `<label>The choice, in one sentence<input name="claim" maxlength="240" value="${draft?.claim}" required></label><label>Why<textarea name="why" rows="3" maxlength="2000" required>${draft?.why}</textarea></label><button type="submit">Record</button>`)}</details>` : '';
40
- return framedSection('Decisions', html `${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${rows.length ? rows : html `<p class="meta">No decisions yet. The lead suggests one when a conversation settles a choice.</p>`}${add}`);
67
+ const rows = decisions.map(d => settingsRecord({
68
+ title: d.claim, sub: d.why, when: html `<span class="k-num">#${d.id}</span> · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}`, attrs: { id: `decision-${d.id}` },
69
+ more: joinHtml([
70
+ note(`${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}`),
71
+ manage ? disclose('Retire', form('retire', joinHtml([
72
+ formRow('Why it no longer holds', null, input({ id: `decision-${d.id}-reason`, name: 'reason', attrs: { maxlength: 500, required: true, 'aria-label': 'Why it no longer holds' } }), 'wide'),
73
+ acts(btn('Retire', { sm: true, variant: 'danger', type: 'submit' })),
74
+ ]), { decision: d.id })) : '',
75
+ ]),
76
+ }));
77
+ const add = manage ? disclose('Record a decision', form('record', joinHtml([
78
+ formRow('The choice, in one sentence', null, input({ id: 'decision-claim', name: 'claim', value: draft?.claim ?? '', attrs: { maxlength: 240, required: true, 'aria-label': 'The choice, in one sentence' } }), 'wide'),
79
+ formRow('Why', null, textarea({ id: 'decision-why', name: 'why', rows: 3, value: draft?.why ?? '', attrs: { maxlength: 2000, required: true, 'aria-label': 'Why' } }), 'top'),
80
+ acts(btn('Record', { sm: true, type: 'submit' })),
81
+ ])), draft?.claim !== undefined) : '';
82
+ return settingsSection({ id: 'decisions', title: 'Decisions', body: joinHtml([error ? notice(error) : '', rows.length ? joinHtml(rows) : note('No decisions yet. The lead suggests one when a conversation settles a choice.'), add]) });
41
83
  }
42
84
  export function memorySearchHtml(repo, query, hits) {
43
- const kind = (h) => h.kind === 'decision' ? html `<a href="#decision-${h.ref}">decision #${h.ref}</a>` : h.kind === 'conversation' ? 'conversation' : h.kind;
44
- const results = hits === null ? '' : hits.length === 0 ? html `<p class="meta">Nothing in project memory matches.</p>` : html `<ul class="memory-hits">${hits.map(h => html `<li><strong>${kind(h)}</strong> · ${h.title}<br><span class="meta">${h.snippet}</span></li>`)}</ul>`;
45
- return framedSection('Search project memory', html `<form class="field-row" method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label><span class="visually-hidden">Search project memory</span><input name="memory" value="${query}" maxlength="300" placeholder="Search decisions, references, lessons…" type="search" autocomplete="off"></label><button type="submit">Search</button></form>${results}`, 'memory-search');
85
+ const kind = (h) => h.kind === 'decision' ? html `<a class="k-link" href="#decision-${h.ref}">Decision #${h.ref}</a>` : h.kind === 'conversation' ? 'Conversation' : h.kind.charAt(0).toUpperCase() + h.kind.slice(1);
86
+ const results = hits === null ? '' : hits.length === 0 ? note('Nothing in project memory matches.') : joinHtml(hits.map(h => settingsRecord({ title: html `${kind(h)} · ${h.title}`, sub: h.snippet })));
87
+ return settingsSection({ id: 'memory-search', title: 'Search project memory', body: joinHtml([
88
+ html `<form method="get" action="/settings/knowledge" class="k-acts k-picker" role="search"><input type="hidden" name="repo" value="${repo}">${input({ id: 'memory-query', name: 'memory', type: 'search', value: query, attrs: { maxlength: 300, placeholder: 'Search decisions, references, lessons…', autocomplete: 'off', 'aria-label': 'Search project memory' } })}${btn('Search', { sm: true, type: 'submit' })}</form>`,
89
+ results,
90
+ ]) });
46
91
  }
47
92
  export function proposalsHtml(repo, proposals, status, csrf, canManage) {
48
93
  const manage = canManage && !!csrf;
49
- const line = status === null ? '' : html `<p class="meta memory-stats"><span>${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''}</span><span>${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'}</span><span>Instructions ${status.instructionBytes}/${status.budgetBytes} bytes</span></p><details class="settings-more"><summary>Read new sessions</summary><p class="meta">Run <code>toolroll memory propose --repo ${repo}</code> on this computer.</p></details>`;
50
- const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', html `<button type="submit">${label}</button>`, { attrs: { class: 'inline' }, hidden: fields({ repo, proposal: p.id, decision }) });
51
- const cards = proposals.map(p => html `<details class="card proposal" open><summary><strong>${p.title}</strong> <span class="meta">#${p.id} · ${p.kind} · ${p.sessions} sessions</span></summary><p>${p.rationale}</p>${p.beforeText ? html `<pre class="knowledge-instructions">- ${p.beforeText}</pre>` : ''}${p.afterText ? html `<pre class="knowledge-instructions">+ ${p.afterText}</pre>` : ''}<ul class="memory-evidence">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” <span class="meta">${ev.session.split(':')[0] ?? ''}</span></li>`)}</ul>${manage ? html `${decide(p, 'accept', 'Accept')} ${decide(p, 'reject', 'Reject')}` : ''}</details>`);
52
- return framedSection('Proposed from sessions', html `${line}${cards.length ? cards : html `<p class="meta">Nothing to review. Suggestions appear when the same mistake shows up in two sessions.</p>`}`);
94
+ const line = status === null ? '' : joinHtml([
95
+ note(`${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''} · ${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'} · Instructions ${status.instructionBytes}/${status.budgetBytes} bytes`),
96
+ disclose('Read new sessions', joinHtml([note('Run this on this computer:'), codeLine(`toolroll memory propose --repo ${repo}`)])),
97
+ ]);
98
+ const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', btn(label, { sm: true, variant: decision === 'accept' ? 'outline' : 'ghost', type: 'submit' }), { hidden: fields({ repo, proposal: p.id, decision }) });
99
+ const cards = proposals.map(p => settingsRecord({
100
+ title: p.title, sub: html `<span class="k-num">#${p.id}</span> · ${p.kind} · ${p.sessions} sessions`,
101
+ action: manage ? html `${decide(p, 'accept', 'Accept')}${decide(p, 'reject', 'Reject')}` : '',
102
+ more: joinHtml([
103
+ note(p.rationale),
104
+ p.beforeText ? html `<pre class="k-scode">- ${p.beforeText}</pre>` : '',
105
+ p.afterText ? html `<pre class="k-scode">+ ${p.afterText}</pre>` : '',
106
+ html `<ul class="k-note">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” · ${ev.session.split(':')[0] ?? ''}</li>`)}</ul>`,
107
+ ]),
108
+ }));
109
+ return settingsSection({ id: 'proposals', title: 'Proposed from sessions', body: joinHtml([line, cards.length ? joinHtml(cards) : note('Nothing to review. Suggestions appear when the same mistake shows up in two sessions.')]) });
53
110
  }
@@ -8,6 +8,7 @@ import { dirname, join } from 'node:path';
8
8
  import { projectOutcomes, projectOutcomesPage } from './project-outcomes.js';
9
9
  import { repositoryContextRead } from './repository-context.js';
10
10
  import { assignmentCatchUp } from './assignment-brief.js';
11
+ import { browserViewOf } from './flow-browser.js';
11
12
  import { CHAT_ACTIONS, CHAT_ACTION_FIELDS, isChatAction, prepareSharedAction, sharedActionNeedsReview, sharedActionPayload } from './chat-actions.js';
12
13
  import { conversationSkills, skillsView } from "./project-skills.js";
13
14
  import { readAcceptanceEvidence } from "./chat-acceptance.js";
@@ -39,7 +40,8 @@ import { CHAT_TASK_ACTIONS, chatTaskRun, chatTaskStamp, isChatTaskAction } from
39
40
  * projects after scrubbing; full paths and arbitrary text remain redacted.
40
41
  */
41
42
  import { Buffer } from "node:buffer";
42
- import { LEAD_ASK_OTHER } from "./store.js";
43
+ import { LEAD_ASK_OTHER, refArchived } from "./store.js";
44
+ import { releaseFactsOf, tidyDigest, tidyGroups, tidyPreview, TIDY_DEFAULT_DAYS } from "./tidy.js";
43
45
  import { hasDisguisedText, hasForbiddenControls } from "./decision.js";
44
46
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
45
47
  import { parseAcceptanceCriteria } from "./scope.js";
@@ -913,13 +915,19 @@ const LEAD_TOOL_HANDLERS = {
913
915
  const active = cards.filter(one => one.state === "active"), finished = cards.filter(one => one.state !== "active").slice(-10);
914
916
  return { ok: true, body: {
915
917
  flow: flow.id, name: flow.name, project: repoIdOf(ctx.who, flow.repo),
918
+ // Browser use (flow-browser.ts), only for a flow whose script zones start one. Names never reach the model.
919
+ ...(() => {
920
+ const view = browserViewOf(ctx.store, flow);
921
+ return view === null ? {} : { browser: view.status === "allowed" ? "allowed: its browser zones may start Chromium (turn_off_browser stops it)"
922
+ : `${view.status === "ended" ? "approval ended: the flow or a script changed" : "not allowed"}; its browser zones wait. Propose allow_browser: a person allows it with their own password in the secure screen.` };
923
+ })(),
916
924
  steps: definition.stages.map(stage => ({
917
925
  id: stage.id, title: stage.title, does: FLOW_KIND_WORDS[stage.kind].label, kind: stage.kind,
918
926
  ...(stage.instructions === null ? {} : { instructions: stage.instructions.slice(0, 600) }),
919
927
  ...(stage.kind === "task" ? { planning: stage.planning } : {}),
920
928
  ...(stage.kind === "approval" ? { decider: stage.toOwner === true ? `the flow's owner (${flow.owner === ctx.who.name ? "you" : "someone else"})` : stage.approver === null ? "anyone who approves" : stage.approver === ctx.who.name ? "you" : "someone else" } : {}),
921
929
  ...(stage.message === null ? {} : { message: stage.message }),
922
- ...(stage.kind === "check" ? { script: stage.script, scriptExists: stage.script !== null && ctx.store.flowScript(flow.repo, stage.script) !== null } : {}),
930
+ ...(stage.kind === "check" ? { script: stage.script, scriptExists: stage.script !== null && ctx.store.flowScript(flow.repo, stage.script) !== null, ...(stage.browser === true ? { startsBrowser: true } : {}) } : {}),
923
931
  ...(stage.kind === "update" ? { closesIssue: stage.close === true } : {}),
924
932
  ...(stage.request === undefined ? {} : { method: stage.request.method, url: stage.request.url, headers: Object.keys(stage.request.headers), ...(stage.request.body === null ? {} : { body: stage.request.body.slice(0, 600) }) }),
925
933
  ...(stage.email === undefined ? {} : { to: stage.email.to, subject: stage.email.subject, body: stage.email.body.slice(0, 600) }),
@@ -985,7 +993,7 @@ const LEAD_TOOL_HANDLERS = {
985
993
  },
986
994
  },
987
995
  propose_flow: {
988
- description: "Draft a flow change as a card the operator confirms. To make a flow, create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about; comment (with @name to ping someone), assign and follow here too. For triage, spam, lead, effort or exception routing, start from the matching template, with the sort as the first step. Use remindAfter and thenMoveTo for follow-ups and decisions that stall. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight, review what went wrong every morning), offer the matching starter; each is one yes, and none merges without a person. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a kit sets up the subagent and its flow in one card. Subagents: propose_subagent adds one (from a template: support, sales, ops, triage), changes one rule section (edit_section), pauses, resumes or removes it, passes it a note, or answers its question for the operator; put it to work with a flow here. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Subagents use the project's tools under a rule per action (do it, ask first: the operator approves each exact call, never, or up to a limit on a number); propose_subagent use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. forget or edit_memory when the operator says a subagent has something wrong; a rule change it suggests is a question for the operator, answered only as they say. propose_subagent add_routine puts a card on its desk on a schedule; propose_subagent undo undoes a tool call only when its action has an undo set (tool_rule undoWith). Actions: starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning, plane-review reviews the plane's last 24 hours every morning and turns each problem worth fixing into a researched fix and a pull request) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a subagent, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). subagents (get_subagents): an approval step with subagent (its short name) is decided by that subagent within its rules, and it hands hard ones to the step's decider; a 'subagent' step (subagent, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain); plane-review: at (HH:MM, default 07:30), timeZone — every day it reads the plane's last 24 hours and makes one card per problem worth fixing, a returning problem joining its card); pause_trigger, resume_trigger, remove_trigger with trigger. A 'send' step ('Send to me') sends the card's owner what the step before produced (summary, links, screenshots) in their chat apps, then moves on; a 'choose' step sends the same with 2 to 4 options (label, goesTo: a step, or 'end' to ignore the card) as buttons, ifReplied (where a reply goes as {{note}}; the build before by default), and remindAfter with ifNoReply. choose (card, and choice by number or a note to reply) answers one for the operator. A 'task' step may build in another project: repo (from list_repos). Read get_flows first except to create.",
996
+ description: "Draft a flow change as a card the operator confirms. To make a flow, create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about; comment (with @name to ping someone), assign and follow here too. For triage, spam, lead, effort or exception routing, start from the matching template, with the sort as the first step. Use remindAfter and thenMoveTo for follow-ups and decisions that stall. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight, review what went wrong every morning), offer the matching starter; each is one yes, and none merges without a person. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a kit sets up the subagent and its flow in one card. Subagents: propose_subagent adds one (from a template: support, sales, ops, triage), changes one rule section (edit_section), pauses, resumes or removes it, passes it a note, or answers its question for the operator; put it to work with a flow here. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Subagents use the project's tools under a rule per action (do it, ask first: the operator approves each exact call, never, or up to a limit on a number); propose_subagent use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. forget or edit_memory when the operator says a subagent has something wrong; a rule change it suggests is a question for the operator, answered only as they say. propose_subagent add_routine puts a card on its desk on a schedule; propose_subagent undo undoes a tool call only when its action has an undo set (tool_rule undoWith). Actions: starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning, plane-review reviews the plane's last 24 hours every morning and turns each problem worth fixing into a researched fix and a pull request) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a subagent, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). subagents (get_subagents): an approval step with subagent (its short name) is decided by that subagent within its rules, and it hands hard ones to the step's decider; a 'subagent' step (subagent, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain); plane-review: at (HH:MM, default 07:30), timeZone — every day it reads the plane's last 24 hours and makes one card per problem worth fixing, a returning problem joining its card); pause_trigger, resume_trigger, remove_trigger with trigger. A 'send' step ('Send to me') sends the card's owner what the step before produced (summary, links, screenshots) in their chat apps, then moves on; a 'choose' step sends the same with 2 to 4 options (label, goesTo: a step, or 'end' to ignore the card) as buttons, ifReplied (where a reply goes as {{note}}; the build before by default), and remindAfter with ifNoReply. choose (card, and choice by number or a note to reply) answers one for the operator. A 'task' step may build in another project: repo (from list_repos). Read get_flows first except to create. allow_browser (flow): when get_flows says a flow's browser zones wait, propose allowing them to start Chromium; the operator allows it with their own password in the secure screen, never on the card. turn_off_browser (flow) stops it.",
989
997
  // An edit's kept steps may leave their kind and name to the zone they keep: they are filled in before the call is read.
990
998
  prepare: (ctx, given) => {
991
999
  const editing = given["operation"] === "edit" && Number.isSafeInteger(given["flow"]) ? ctx.store.getFlow(Number(given["flow"])) : null;
@@ -1125,7 +1133,16 @@ const LEAD_TOOL_HANDLERS = {
1125
1133
  operation = "flow_trigger_remove";
1126
1134
  input = pick(["trigger"]);
1127
1135
  break;
1128
- default: return { ok: false, message: "Choose create, edit, add_card, move_card, approve, send_back, cancel_card, comment, assign, follow, unfollow, add_trigger, pause_trigger, resume_trigger or remove_trigger." };
1136
+ // Browser use in the flow's script zones: a proposal only; the person allows it with their own password.
1137
+ case "allow_browser":
1138
+ operation = "flow_browser";
1139
+ input = pick(["flow"]);
1140
+ break;
1141
+ case "turn_off_browser":
1142
+ operation = "flow_browser_off";
1143
+ input = pick(["flow"]);
1144
+ break;
1145
+ default: return { ok: false, message: "Choose create, edit, add_card, move_card, approve, send_back, cancel_card, comment, assign, follow, unfollow, add_trigger, pause_trigger, resume_trigger, remove_trigger, allow_browser or turn_off_browser." };
1129
1146
  }
1130
1147
  const action = prepareSharedAction(ctx.store, ctx.who, operation, input, ctx.evidenceRoot, ctx.now);
1131
1148
  const id = ctx.draft("action", { ...action });
@@ -1608,11 +1625,12 @@ const LEAD_TOOL_HANDLERS = {
1608
1625
  return { ok: false, message: "search needs a word that names the work, such as a page, feature or file" };
1609
1626
  // A search reads further back than the newest page, then ranks by how many of the words each task's title and goal hold.
1610
1627
  const listed = ctx.store.taskFamiliesAdmitted(repo === null ? ctx.who.repos : [repo], false, { limit: words.length === 0 ? limit + 1 : SEARCH_FAMILIES, order: words.length === 0 ? "created" : "updated", ...(args["state"] === undefined ? {} : { states: [args["state"]] }) });
1611
- const families = words.length === 0 ? listed : listed
1628
+ // A put-away task (v145) is out of every list, the lead's included.
1629
+ const families = (words.length === 0 ? listed : listed
1612
1630
  .map((one, index) => ({ one, index, hits: searchHits(words, [one.root.id, one.root.title, one.current.title, ctx.store.getScope(one.current.id)?.goal ?? "", ctx.store.getScope(one.root.id)?.goal ?? ""]) }))
1613
1631
  .filter(match => match.hits > 0)
1614
1632
  .sort((a, b) => b.hits - a.hits || a.index - b.index)
1615
- .map(match => match.one);
1633
+ .map(match => match.one)).filter(one => !refArchived(ctx.store, one.current.refId));
1616
1634
  const tasks = families.slice(0, limit).map(one => ({
1617
1635
  repo: `r${ctx.who.repos.indexOf(one.current.repo) + 1}`, task: one.root.id, execution: one.current.id,
1618
1636
  title: one.root.title, state: one.current.state, historyProblem: one.problem, otherActive: one.otherActive.map(version => version.id),
@@ -1622,6 +1640,17 @@ const LEAD_TOOL_HANDLERS = {
1622
1640
  return { ok: true, body: { tasks, truncated: families.length > limit } };
1623
1641
  },
1624
1642
  },
1643
+ tidy_preview: {
1644
+ description: "Read-only: what Put away old tasks would put away now and why, grouped (old releases, old routine runs, resolved CI notices, finished more than `days` days ago, default 14). Only a person puts tasks away; you can't.",
1645
+ handle: (ctx, args) => {
1646
+ const days = typeof args["days"] === "number" ? args["days"] : TIDY_DEFAULT_DAYS;
1647
+ const items = tidyPreview(ctx.store, ctx.now, { releases: releaseFactsOf(ctx.store), days, access: { principal: "operator", repos: ctx.who.repos, includeUnplaced: false } });
1648
+ const digest = tidyDigest(items, days);
1649
+ return { ok: true, body: { count: items.length, days, digest,
1650
+ groups: tidyGroups(items, days).map(group => ({ label: group.label, tasks: group.items.map(one => ({ task: one.taskId, title: one.title, why: one.reason })) })),
1651
+ apply: items.length === 0 ? "Nothing to put away." : `A person puts these away: Tasks → Put away old tasks, or \`toolroll task tidy --apply --digest ${digest}${days === TIDY_DEFAULT_DAYS ? "" : ` --days ${days}`}\`.` } };
1652
+ },
1653
+ },
1625
1654
  get_task: {
1626
1655
  description: "Read currentExecution, state, dispatch, history, scope, dependencies, holds, queue, attempts, decisions and whether the agent has read your notes before acting.",
1627
1656
  handle: (ctx, args) => {
@@ -8,7 +8,7 @@
8
8
  import { actorLabel, withActor } from "./actor.js";
9
9
  import { chatControlHref } from "./chat-controls.js";
10
10
  import { BATCH_MS, chatText, chatTitle } from "./chat-voice.js";
11
- import { BUILT_IN } from "./store.js";
11
+ import { BUILT_IN, refArchived } from "./store.js";
12
12
  import { leadNameOf } from "./lead-identity.js";
13
13
  import { leadLapsed } from "./task-status.js";
14
14
  import { phoneText as plain } from "./telegram-status.js";
@@ -85,19 +85,24 @@ export function leadSay(store, actor, text, taskId, now) {
85
85
  // The task's own id never reaches a chat: it reads as the task's short title (chat-voice.ts).
86
86
  const said = taskId === null ? text.trim() : chatText(text.trim(), [{ id: taskId, title: chatTitle(store, taskId) }]);
87
87
  return store.transact(() => {
88
+ // A put-away task (v145) gets no messages: the lead's words about it would nudge a person about nothing.
89
+ if (task !== null && refArchived(store, task.refId))
90
+ return { ok: false, reason: "refused", message: "This task is put away. A person can restore it first." };
88
91
  const stamp = now.toISOString();
89
92
  const ledger = store.recordAction({ at: stamp, actor: actorLabel(actor), repo: task?.repo ?? null, taskId, runId: null,
90
93
  action: LEAD_SAID, outcome: "recorded", source: "work", detail: text.trim() });
91
- const open = store.handle.prepare(`SELECT id, dedupe_key, body FROM notification WHERE kind = ? AND recipient = ? ORDER BY id DESC LIMIT 1`)
94
+ const open = store.handle.prepare(`SELECT id, dedupe_key, body, task_ref FROM notification WHERE kind = ? AND recipient = ? ORDER BY id DESC LIMIT 1`)
92
95
  .get(LEAD_SAY_KIND, actor.account);
93
96
  const opened = open === undefined ? null : /^lead-say:(.+):\d+$/.exec(String(open["dedupe_key"]))?.[1] ?? null;
94
- const joined = open !== undefined && opened !== null && Date.parse(opened) > now.getTime() - BATCH_MS && Date.parse(opened) <= now.getTime();
97
+ // A batch is about one task (or none): words about another start a row of their own, so each row keeps its task.
98
+ const sameTask = open !== undefined && (open["task_ref"] ?? null) === (task === null ? null : task.refId);
99
+ const joined = sameTask && opened !== null && Date.parse(opened) > now.getTime() - BATCH_MS && Date.parse(opened) <= now.getTime();
95
100
  const lines = [...(joined ? String(open["body"]).split("\n") : []), said].slice(-BATCH_LINES);
96
101
  if (joined)
97
102
  store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE id = ? AND resolved_at IS NULL").run(stamp, Number(open["id"]));
98
103
  const key = `lead-say:${joined ? opened : stamp}:${ledger}`;
99
104
  store.enqueueNotification({ dedupeKey: key, kind: LEAD_SAY_KIND, subject: leadNameOf(store, actor.account), body: lines.join("\n"),
100
- recipient: actor.account, source: { installation: true }, ...(taskId === null ? {} : { link: chatControlHref("task", taskId) }) }, now);
105
+ recipient: actor.account, source: task === null ? { installation: true } : { taskRef: task.refId }, ...(taskId === null ? {} : { link: chatControlHref("task", taskId) }) }, now);
101
106
  const row = store.handle.prepare("SELECT id FROM notification WHERE dedupe_key = ?").get(key);
102
107
  return { ok: true, joined, notification: row === undefined ? null : Number(row["id"]) };
103
108
  });
@@ -136,14 +141,20 @@ export function leadClaim(store, actor, taskId, now) {
136
141
  if ("ok" in task)
137
142
  return task;
138
143
  const { root } = familyOf(store, taskId);
139
- store.transact(() => {
144
+ const taken = store.transact(() => {
145
+ // A put-away task (v145) waits on nobody, so the lead takes nothing on there: checked with the claim's own write.
146
+ if (refArchived(store, task.refId))
147
+ return false;
140
148
  const ledger = store.recordAction({ at: now.toISOString(), actor: actorLabel(actor), repo: task.repo, taskId: root, runId: null,
141
149
  action: LEAD_CLAIMED, outcome: "recorded", source: "work" });
142
150
  // The owner's card for this task repaints to "<name> is on it": quiet chat edits its card, and every-update
143
151
  // chat edits the attempt's own card (the Failed alert), or follows it up when that message is gone.
144
152
  store.enqueueNotification({ dedupeKey: `lead-on-it:${ledger}`, kind: LEAD_ON_IT_KIND, subject: `${leadNameOf(store, actor.account)} is on it`, body: "",
145
153
  link: chatControlHref("task", taskId), source: cardSource(store, root) ?? { taskRef: task.refId } }, now);
154
+ return true;
146
155
  });
156
+ if (!taken)
157
+ return { ok: false, reason: "refused", message: "This task is put away. A person can restore it first." };
147
158
  return { ok: true, root, claim: leadClaimOf(store, taskId, now, actor.account) };
148
159
  }
149
160
  /** Where a task's chat card lives: the family's newest attempt (the card every-update chat edits), else its newest
@@ -22,4 +22,6 @@ export type LedgerChainView = {
22
22
  /** Today (UTC, YYYY-MM-DD): the audit export's default range ends here. */
23
23
  today: string;
24
24
  };
25
+ /** The audit log (`/ledger`): what people and tokens did, newest first, as quiet timed rows, with the filters, the CSV
26
+ * and the sealed export. A kit list page: the top bar, then rows between hairlines. */
25
27
  export declare function ledgerBody(rows: LedgerEntry[], projects: readonly string[], params: URLSearchParams, chain?: LedgerChainView): Html;