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
@@ -4,6 +4,7 @@ import type { WritesMode } from "./writes-mode.js";
4
4
  import { type CheckAccess } from "./check-access.js";
5
5
  import { type Html } from "./html.js";
6
6
  import { type ProjectHoldings } from "./project-delete.js";
7
+ /** Check access: a wider level's warning shows only under the option chosen. Everything else on the page is kit markup. */
7
8
  export declare const PROJECT_DELETE_CSS: string;
8
9
  export type ProjectSettingsView = {
9
10
  remoteUrl?: string | null;
@@ -68,8 +69,9 @@ export type ProjectSettingsView = {
68
69
  signInsUnavailable?: string | null;
69
70
  };
70
71
  };
71
- /** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
72
- export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
72
+ /** The project's page: its name and path, what Toolroll holds for it, its build settings, and (for an instance operator)
73
+ * Remove and Delete. */
74
+ export declare function projectSettingsHtml(view: ProjectSettingsView, said: {
73
75
  said?: string | null;
74
76
  problem?: string | null;
75
77
  }): Html;
@@ -6,55 +6,34 @@
6
6
  */
7
7
  import { sandboxRuntimeWords } from "./sandbox-runtime.js";
8
8
  import { CHECK_ACCESS_WARNINGS, signInHostsMissing } from "./check-access.js";
9
- import { html, postForm, reauth } from "./html.js";
9
+ import { html, joinHtml, postForm } from "./html.js";
10
10
  import { holdingsWords } from "./project-delete.js";
11
- export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:720px;min-width:0}.project-settings .path,.project-delete .path{font-family:var(--font-mono);font-size:.8125rem;overflow-wrap:anywhere;word-break:break-all}` +
12
- `.project-settings details.danger-zone{margin-top:24px;border:1px solid var(--border);border-radius:10px;padding:0 14px}.project-settings details.danger-zone summary{min-height:44px;display:flex;align-items:center;cursor:pointer;font-weight:500;color:var(--danger)}` +
13
- `.project-settings details.danger-zone[open]{padding-bottom:14px}.project-settings form,.project-delete form{display:grid;gap:12px;margin-top:12px}.project-settings label,.project-delete label{display:grid;gap:4px;font-size:.8125rem;font-weight:500}` +
14
- `.project-settings input,.project-delete input{max-width:100%;min-height:36px}.project-delete .removes{margin:12px 0;padding-left:20px}.project-delete .removes li{margin:2px 0}` +
15
- `.project-delete .actions{display:flex;flex-wrap:wrap;gap:12px;align-items:center}.project-delete button.danger,.project-settings button.danger{background:var(--danger);color:#fff;border-color:var(--danger);white-space:nowrap}` +
16
- `.project-settings button,.project-delete button{justify-self:start;min-height:36px;white-space:nowrap}.project-delete .actions a{min-height:36px;display:inline-flex;align-items:center}` +
17
- `.project-settings .builds-at-once{margin-top:20px}.project-settings .builds-at-once h2{font-size:1rem;margin:0 0 4px}.project-settings .builds-at-once form{margin-top:4px;gap:4px}` +
18
- `.project-settings .builds-at-once .builds-row{display:flex;gap:8px;align-items:stretch}.project-settings .builds-at-once input{width:5.5rem;min-height:44px;margin:0;box-sizing:border-box}.project-settings .builds-at-once button{min-height:44px;margin:0}` +
19
- `.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}` +
20
- `.project-settings .build-access{margin-top:20px;min-width:0;overflow-wrap:anywhere}.project-settings .build-access h2{font:var(--so-typography-title-sm);margin:0 0 4px}.project-settings .build-access p{margin:0}` +
21
- `.project-settings .build-access .tool-name{font:var(--so-typography-mono)}.project-settings .build-access details{margin:4px 0 0}.project-settings .build-access summary{min-height:44px;display:inline-flex;align-items:center;cursor:pointer;color:var(--so-muted);font:var(--so-typography-label)}` +
22
- `.project-settings .build-access ul{margin:0 0 4px;padding-left:20px;font:var(--so-typography-body);color:var(--so-ink)}.project-settings .build-access li{margin:2px 0}.project-settings .build-access li .meta{color:var(--so-muted)}` +
23
- `.project-settings .build-access a.manage-tools{display:inline-flex;align-items:center;justify-content:center;min-height:44px;margin-top:4px;padding:0 14px;white-space:nowrap;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line);border-radius:var(--so-radius-md);font:var(--so-typography-label);text-decoration:none}` +
24
- // A radio or checkbox choice sits beside its words, at its natural size, not as a full-width grid row.
25
- `.project-settings .build-access label.check{display:flex;align-items:center;gap:8px;min-height:44px;font:var(--so-typography-body);font-weight:500}` +
26
- `.project-settings .build-access label.check input{width:auto;min-height:0;margin:0;flex:none}.project-settings .build-access form .meta{margin:-8px 0 0 26px}` +
27
- // Check access: a warning shows only under the option chosen; the folds draw the console's own chevron (as a diff's).
28
- `.project-settings .check-access-setting .option .warning{display:none;color:var(--so-ink)}.project-settings .check-access-setting .option:has(input:checked) .warning{display:block}` +
29
- `.project-settings .check-access-setting form{margin-top:0;gap:10px}.project-settings .check-access-setting .option{display:grid;gap:2px}` +
30
- `.project-settings .check-access-setting .option .meta{margin:0 0 0 26px}.project-settings .check-access-setting label.check{min-height:36px}` +
31
- `.project-settings .check-access-setting details > summary{list-style:none;display:flex;align-items:center;gap:10px;min-height:40px;padding:0}` +
32
- `.project-settings .check-access-setting details > summary::-webkit-details-marker{display:none}.project-settings .check-access-setting details > summary::marker{content:""}` +
33
- `.project-settings .check-access-setting details > summary::before{content:"";flex:none;width:.35rem;height:.35rem;border-right:1.5px solid var(--so-muted);border-bottom:1.5px solid var(--so-muted);transform:rotate(-45deg);transition:transform .15s ease}` +
34
- `.project-settings .check-access-setting details[open] > summary::before{transform:rotate(45deg) translateY(-.1rem)}` +
35
- // Show hosts: a quiet fold in the line, not a card.
36
- `.project-settings .check-access-setting details.hosts{border:0;background:none;padding:0;margin:0}.project-settings .check-access-setting details.hosts > summary{min-height:32px;font-size:.8125rem}` +
37
- `.project-settings .check-access-setting details.hosts .tool-name{margin:0 0 4px;font-size:.75rem}` +
38
- `.project-settings .check-access-setting button.primary{background:var(--so-signal);color:var(--so-on-signal);border-color:var(--so-signal)}` +
39
- `.project-settings .build-access a.manage-tools:hover{background:var(--so-raised)}.project-settings .build-access a.manage-tools:focus-visible,.project-settings .build-access summary:focus-visible{outline:2px solid var(--so-ink);outline-offset:2px}`;
40
- /** Where builds may write, said plainly; an approver changes it with their password. */
11
+ import { acts, btn, codeLine, disclose, formRow, infoLine, input, note, notice, radios, settingsSection, switchInput, textarea } from "./server/kit.js";
12
+ import { kitSectionForm } from "./settings-kit.js";
13
+ /** Check access: a wider level's warning shows only under the option chosen. Everything else on the page is kit markup. */
14
+ export const PROJECT_DELETE_CSS = ".k-opt [data-check-access-warning]{display:none;color:var(--so-ink)}.k-opt:has(input:checked) [data-check-access-warning]{display:block}" +
15
+ ".k-opt:has(input:disabled){cursor:default;opacity:.6}";
16
+ const mono = (text) => html `<span class="k-mono">${text}</span>`;
17
+ const list = (items) => html `<ul class="k-note">${items.map(one => html `<li>${one}</li>`)}</ul>`;
18
+ const SCOPED = "Tasks scoped from now on use this; tasks already approved keep what they were approved with.";
19
+ const APPROVER_CHANGES = note("An approver for this project can change this.");
20
+ /** Where builds may write, said plainly; an approver changes it with their password (always typed: it widens). */
41
21
  function writesHtml(view) {
42
22
  const writes = view.writes;
43
23
  if (writes === undefined)
44
24
  return "";
45
25
  const said = writes.setting === "workspace"
46
- ? html `<p>Builds write <strong>only their own checkout</strong>, its caches and its temp folder. Your shell profile, login items, agent settings and the repository's Git config and hooks stay as they are.</p>`
47
- : html `<p>Builds may write <strong>anywhere</strong> outside Toolroll's own folders, your credentials and other projects.</p>`;
48
- if (!writes.canChange)
49
- return html `<section class="build-access writes-setting" id="writes" aria-labelledby="writes-title"><h2 id="writes-title">Where builds write</h2>${said}<p class="meta">An approver for this project can change this.</p></section>`;
50
- const option = (value, label, detail) => html `<label class="check"><input type="radio" name="writes" value="${value}"${writes.setting === value ? html ` checked` : ""}> ${label}</label><p class="meta">${detail}</p>`;
51
- const form = postForm("/settings/project/writes", html `
52
- ${option("workspace", "Only their workspace (recommended)", "The checkout and its Git data, the project's own package caches, the build's temp folder, and what its agent needs to keep its session.")}
53
- ${option("anywhere", "Anywhere outside Toolroll's fenced folders", "For a project whose tools must write elsewhere in your home folder or the shared temp folder. Credentials, Toolroll's own folders and other projects stay fenced.")}
54
- <p class="meta">Tasks scoped from now on use this; tasks already approved keep what they were approved with.</p>
55
- ${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
56
- <button type="submit">Save</button>`, { hidden: { repo: view.repo } });
57
- return html `<section class="build-access writes-setting" id="writes" aria-labelledby="writes-title"><h2 id="writes-title">Where builds write</h2>${said}<details><summary>Change</summary>${form}</details></section>`;
26
+ ? "Builds write only their own checkout, its caches and its temp folder. Your shell profile, login items, agent settings and the repository's Git config and hooks stay as they are."
27
+ : "Builds may write anywhere outside Toolroll's own folders, your credentials and other projects.";
28
+ return settingsSection({ id: "writes", title: "Where builds write", state: writes.setting === "workspace" ? " · Only their workspace" : " · Anywhere", lead: said,
29
+ body: !writes.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-writes", action: "/settings/project/writes", id: "writes-form", gate: true, hidden: { repo: view.repo },
30
+ body: joinHtml([
31
+ formRow("Builds may write", null, radios({ name: "writes", legend: "Where builds write", current: writes.setting, items: [
32
+ { value: "workspace", label: "Only their workspace (recommended)", text: "The checkout and its Git data, the project's own package caches, the build's temp folder, and what its agent needs to keep its session." },
33
+ { value: "anywhere", label: "Anywhere outside Toolroll's fenced folders", text: "For a project whose tools must write elsewhere in your home folder or the shared temp folder. Credentials, Toolroll's own folders and other projects stay fenced." }
34
+ ] }), "wide"),
35
+ note(SCOPED),
36
+ ]) })) });
58
37
  }
59
38
  /** Home folder (project-sandbox.ts): what builds and checks can read there, said once; the paths they can't, and the
60
39
  * advanced list (macOS services), changed under Change with the approver's password. */
@@ -64,24 +43,20 @@ function sandboxHtml(view) {
64
43
  return "";
65
44
  const { readDeny, machLookup } = sandbox.setting;
66
45
  const said = readDeny.length === 0
67
- ? html `<p>Builds and checks can read your home folder, as in Anthropic's sandbox runtime. Toolroll's own folders stay out of reach; other tasks' work is read, never written.</p>`
68
- : html `<p>Builds and checks can read your home folder except <strong>${readDeny.length === 1 ? readDeny[0] : `${readDeny.length} paths`}</strong>.</p>`;
69
- const extra = machLookup.length === 0 ? "" : html `<p class="meta">${machLookup.length} macOS ${machLookup.length === 1 ? "service" : "services"} allowed</p>`;
70
- // The engine, named where the sandbox is set (sandbox-runtime.ts).
71
- const engine = html `<p class="meta" data-sandbox-engine>Builds, checks and flows run in ${sandboxRuntimeWords()}.</p>`;
72
- const head = html `<h2 id="sandbox-title">Home folder</h2>${said}${extra}${engine}`;
73
- if (!sandbox.canChange)
74
- return html `<section class="build-access sandbox-setting" id="sandbox" aria-labelledby="sandbox-title">${head}<p class="meta">An approver for this project can change this.</p></section>`;
75
- const form = postForm("/settings/project/sandbox", html `
76
- <label>Paths builds can't read<textarea name="read-deny" rows="4" placeholder="~/.ssh&#10;~/.aws">${readDeny.join("\n")}</textarea></label>
77
- <p class="meta">One per line: <code>~/…</code> or a full path.</p>
78
- <details><summary>Advanced</summary>
79
- <label>macOS services builds may also use<textarea name="mach" rows="2">${machLookup.join("\n")}</textarea></label>
80
- </details>
81
- <p class="meta">Tasks scoped from now on use this; tasks already approved keep what they were approved with.</p>
82
- ${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
83
- <button type="submit">Save</button>`, { hidden: { repo: view.repo } });
84
- return html `<section class="build-access sandbox-setting" id="sandbox" aria-labelledby="sandbox-title">${head}<details><summary>Change</summary>${form}</details></section>`;
46
+ ? "Builds and checks can read your home folder, as in Anthropic's sandbox runtime. Toolroll's own folders stay out of reach; other tasks' work is read, never written."
47
+ : `Builds and checks can read your home folder except ${readDeny.length === 1 ? readDeny[0] : `${readDeny.length} paths`}.`;
48
+ const head = joinHtml([
49
+ machLookup.length === 0 ? "" : note(`${machLookup.length} macOS ${machLookup.length === 1 ? "service" : "services"} allowed`),
50
+ // The engine, named where the sandbox is set (sandbox-runtime.ts).
51
+ html `<p class="k-note" data-sandbox-engine>Builds, checks and flows run in ${sandboxRuntimeWords()}.</p>`,
52
+ ]);
53
+ return settingsSection({ id: "sandbox", title: "Home folder", lead: said,
54
+ body: joinHtml([head, !sandbox.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-sandbox", action: "/settings/project/sandbox", id: "sandbox-form", gate: true, hidden: { repo: view.repo },
55
+ body: joinHtml([
56
+ formRow("Paths builds can't read", "One per line: ~/… or a full path.", textarea({ id: "sandbox-read-deny", name: "read-deny", value: readDeny.join("\n"), rows: 4, attrs: { placeholder: "~/.ssh\n~/.aws", "aria-label": "Paths builds can't read" } }), "top"),
57
+ disclose("Advanced", formRow("macOS services builds may also use", null, textarea({ id: "sandbox-mach", name: "mach", value: machLookup.join("\n"), rows: 2, attrs: { "aria-label": "macOS services builds may also use" } }), "top")),
58
+ note(SCOPED),
59
+ ]) }))]) });
85
60
  }
86
61
  /** Check access (check-access.ts): how far the project's checks reach, said once. Collapsed: the level, what it means
87
62
  * when it isn't the default, and who changed it. Inside Change: a line per level, and each wider level's warning under it
@@ -99,32 +74,41 @@ function checkAccessHtml(view) {
99
74
  const meaning = { sandboxed: null, "sign-ins": "Checks can read your Claude, Codex and GitHub sign-ins.", unsandboxed: "Checks run with your full access to this machine and to Toolroll." };
100
75
  // The sign-ins never widen the network: how many hosts they'd still need, and where to add them (a change of its own).
101
76
  const missing = setting === "sign-ins" && network !== null ? signInHostsMissing(network) : [];
102
- const hosts = missing.length === 0 ? "" : html `<p class="meta" data-sign-in-hosts>${missing.length} sign-in ${missing.length === 1 ? "host isn't" : "hosts aren't"} allowed yet · <a href="#network">Add them</a></p><details class="hosts"><summary>Show hosts</summary><p class="tool-name">${missing.join(", ")}</p></details>`;
103
- const changed = access.changed === null ? "" : html `<p class="meta">Changed by ${access.changed.by}, ${new Date(access.changed.at).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })}</p>`;
104
- // Said before anything else: on a machine with no OS sandbox, every level runs without one.
105
- const unconfined = access.unconfinedHere === true ? html `<p class="meta warning-line" data-unconfined-here>This machine can't sandbox checks, so they run without a sandbox whatever is chosen here.</p>` : "";
106
- const runsSandboxed = setting !== saved && access.unconfinedHere !== true ? html `<p class="meta warning-line" data-sign-ins-unavailable>Saved with model and GitHub sign-ins, but checks run sandboxed: sign-ins can't be shared with checks safely yet.</p>` : "";
77
+ const hosts = missing.length === 0 ? "" : joinHtml([
78
+ html `<p class="k-note" data-sign-in-hosts>${missing.length} sign-in ${missing.length === 1 ? "host isn't" : "hosts aren't"} allowed yet · <a class="k-link" href="#network">Add them</a></p>`,
79
+ disclose("Show hosts", html `<p class="k-note k-mono">${missing.join(", ")}</p>`),
80
+ ]);
81
+ const changed = access.changed === null ? "" : note(`Changed by ${access.changed.by}, ${new Date(access.changed.at).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })}`);
107
82
  // No OS sandbox here, and unconfined running allowed: checks run without a sandbox whatever is saved, and that is
108
83
  // what the page says first (never "sandboxed"); the saved level is named under it.
109
- const head = access.unconfinedHere === true
110
- ? html `<h2 id="check-access-title">Check access</h2><p><strong>${names.unsandboxed}</strong></p>${saved === "unsandboxed" ? "" : html `<p class="meta" data-saved-level>Saved as ${names[saved]}.</p>`}${unconfined}${changed}`
111
- : html `<h2 id="check-access-title">Check access</h2><p><strong>${names[setting]}</strong></p>${meaning[setting] === null ? "" : html `<p class="meta">${meaning[setting]}</p>`}${runsSandboxed}${hosts}${changed}`;
84
+ const unconfinedHere = access.unconfinedHere === true;
85
+ const shown = unconfinedHere ? "unsandboxed" : setting;
86
+ const head = unconfinedHere
87
+ ? joinHtml([saved === "unsandboxed" ? "" : html `<p class="k-note" data-saved-level>Saved as ${names[saved]}.</p>`,
88
+ html `<p class="k-note" data-unconfined-here>This machine can't sandbox checks, so they run without a sandbox whatever is chosen here.</p>`, changed])
89
+ : joinHtml([setting !== saved ? html `<p class="k-note" data-sign-ins-unavailable>Saved with model and GitHub sign-ins, but checks run sandboxed: sign-ins can't be shared with checks safely yet.</p>` : "", hosts, changed]);
90
+ const section = (body) => settingsSection({ id: "check-access", title: "Check access", state: ` · ${names[shown]}`,
91
+ lead: unconfinedHere ? undefined : meaning[setting] ?? undefined, body });
112
92
  if (!access.canNarrow && !access.canWiden)
113
- return html `<section class="build-access check-access-setting" id="check-access" aria-labelledby="check-access-title">${head}</section>`;
93
+ return section(head);
114
94
  const option = (value, detail, warning) => {
115
95
  const allowed = (value === setting || access.canWiden || value === "sandboxed") && !(value === "sign-ins" && unavailable !== null);
116
- return html `<div class="option"><label class="check"><input type="radio" name="level" value="${value}"${setting === value ? html ` checked` : ""}${allowed ? "" : html ` disabled`}> ${names[value]}</label><p class="meta">${detail}</p>${warning === null ? "" : html `<p class="meta warning" data-check-access-warning="${value}">${warning}</p>`}</div>`;
96
+ return html `<label class="k-opt"><input type="radio" name="level" value="${value}"${setting === value ? html ` checked` : ""}${allowed ? "" : html ` disabled`}><span><b>${names[value]}</b><span>${detail}</span>\
97
+ ${warning === null ? "" : html `<span data-check-access-warning="${value}">${warning}</span>`}</span></label>`;
117
98
  };
118
99
  const open = network?.mode === "open";
119
- const form = postForm("/settings/project/check-access", html `
120
- ${option("sandboxed", "Reads your home folder, as in Anthropic's sandbox runtime, except the folders this project hides (Home folder, above). Never Toolroll's folders or other tasks' work.", null)}
121
- ${unavailable !== null ? option("sign-ins", unavailable, null) : option("sign-ins", `Also reads the files your Claude, Codex and GitHub sign-ins are kept in, on this project's network (${open ? "any host" : network === null || network.hosts.length === 0 ? "registries only" : `registries and ${network.hosts.length} ${network.hosts.length === 1 ? "host" : "hosts"}`}).`, CHECK_ACCESS_WARNINGS["sign-ins"])}
122
- ${option("unsandboxed", "For projects whose checks create sandboxes themselves, such as Toolroll's own.", CHECK_ACCESS_WARNINGS.unsandboxed)}
123
- ${open && unavailable === null ? html `<label class="check"><input type="checkbox" name="confirm-open" value="yes"> The network is open: with sign-ins, checks could send them to any host</label>` : ""}
124
- ${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
125
- <button type="submit" class="primary">Save</button>
126
- <p class="meta">${access.canWiden ? "" : "Only an instance operator can widen this. "}Flow script steps always run sandboxed. Tasks already approved keep their level.</p>`, { hidden: { repo: view.repo } });
127
- return html `<section class="build-access check-access-setting" id="check-access" aria-labelledby="check-access-title">${head}<details class="change"><summary>Change</summary>${form}</details></section>`;
100
+ const choices = html `<fieldset class="k-radios"><legend class="k-sr">Check access</legend>\
101
+ ${option("sandboxed", "Reads your home folder, as in Anthropic's sandbox runtime, except the folders this project hides (Home folder, above). Never Toolroll's folders or other tasks' work.", null)}\
102
+ ${unavailable !== null ? option("sign-ins", unavailable, null) : option("sign-ins", `Also reads the files your Claude, Codex and GitHub sign-ins are kept in, on this project's network (${open ? "any host" : network === null || network.hosts.length === 0 ? "registries only" : `registries and ${network.hosts.length} ${network.hosts.length === 1 ? "host" : "hosts"}`}).`, CHECK_ACCESS_WARNINGS["sign-ins"])}\
103
+ ${option("unsandboxed", "For projects whose checks create sandboxes themselves, such as Toolroll's own.", CHECK_ACCESS_WARNINGS.unsandboxed)}</fieldset>`;
104
+ const confirmOpen = open && unavailable === null
105
+ ? html `<label class="k-opt"><input type="checkbox" name="confirm-open" value="yes"><span><b>The network is open</b><span>With sign-ins, checks could send them to any host.</span></span></label>`
106
+ : "";
107
+ // A form that offers a wider level asks for an instance operator's password, typed now (the server decides).
108
+ const form = kitSectionForm({ proof: access.canWiden ? "project-check-access-widen" : "project-check-access", action: "/settings/project/check-access", id: "check-access-form", gate: true, hidden: { repo: view.repo },
109
+ body: joinHtml([formRow("How far checks reach", null, choices, "wide"), confirmOpen,
110
+ note(`${access.canWiden ? "" : "Only an instance operator can widen this. "}Flow script steps always run sandboxed. Tasks already approved keep their level.`)]) });
111
+ return section(joinHtml([head, disclose("Change", form)]));
128
112
  }
129
113
  /** Network: what the project's builds may reach, said plainly; an approver changes it with their password. */
130
114
  function networkHtml(view) {
@@ -133,22 +117,20 @@ function networkHtml(view) {
133
117
  return "";
134
118
  const { setting } = network;
135
119
  const reach = setting.mode === "open"
136
- ? html `<p>Builds may reach <strong>any host</strong>.</p>`
137
- : html `<p>Builds reach their agent's provider and the package registries this project uses${setting.hosts.length === 0 ? "" : html `, and <span class="tool-name">${setting.hosts.join(", ")}</span>`}. Everything else is refused.</p>`;
120
+ ? html `Builds may reach any host.`
121
+ : html `Builds reach their agent's provider and the package registries this project uses${setting.hosts.length === 0 ? "" : html `, and ${mono(setting.hosts.join(", "))}`}. Everything else is refused.`;
138
122
  const local = setting.loopback === "any"
139
- ? html `<p>On a Mac, builds may reach <strong>this Mac's local services</strong>: test servers, and anything else listening here.</p>`
140
- : html `<p>On a Mac, builds reach no local services. A project whose tests start a local server needs them on.</p>`;
141
- if (!network.canChange)
142
- return html `<section class="build-access network-setting" id="network" aria-labelledby="network-title"><h2 id="network-title">Network</h2>${reach}${local}<p class="meta">An approver for this project can change this.</p></section>`;
143
- const form = postForm("/settings/project/network", html `
144
- <label class="check"><input type="checkbox" name="local" value="any"${setting.loopback === "any" ? html ` checked` : ""}> Builds may reach this Mac's local services</label>
145
- <p class="meta">Local test servers work, and so does anything else listening on this Mac (a local database, a dev proxy). Linux builds always have local services of their own.</p>
146
- <label>Other hosts builds may reach<textarea name="hosts" rows="3" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="api.stripe.com&#10;*.example.com">${setting.hosts.join("\n")}</textarea></label>
147
- <label class="check"><input type="checkbox" name="mode" value="open"${setting.mode === "open" ? html ` checked` : ""}> Open: builds may reach any host</label>
148
- <p class="meta">Tasks scoped from now on use this; tasks already approved keep the network they were approved with.</p>
149
- ${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
150
- <button type="submit">Save</button>`, { hidden: { repo: view.repo } });
151
- return html `<section class="build-access network-setting" id="network" aria-labelledby="network-title"><h2 id="network-title">Network</h2>${reach}${local}<details><summary>Change</summary>${form}</details></section>`;
123
+ ? note("On a Mac, builds may reach this Mac's local services: test servers, and anything else listening here.")
124
+ : note("On a Mac, builds reach no local services. A project whose tests start a local server needs them on.");
125
+ return settingsSection({ id: "network", title: "Network", state: setting.mode === "open" ? " · Open" : "", lead: reach,
126
+ body: joinHtml([local, !network.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-network", action: "/settings/project/network", id: "network-form", gate: true, hidden: { repo: view.repo },
127
+ body: joinHtml([
128
+ formRow("This Mac's local services", "Local test servers work, and so does anything else listening on this Mac (a local database, a dev proxy). Linux builds always have local services of their own.", switchInput({ id: "network-local", name: "local", label: "Allowed", on: setting.loopback === "any", attrs: { value: "any", "aria-label": "Builds may reach this Mac's local services" } }), "inline"),
129
+ formRow("Other hosts builds may reach", "One per line.", textarea({ id: "network-hosts", name: "hosts", value: setting.hosts.join("\n"), rows: 3,
130
+ attrs: { autocomplete: "off", autocapitalize: "off", spellcheck: "false", placeholder: "api.stripe.com\n*.example.com", "aria-label": "Other hosts builds may reach" } }), "top"),
131
+ formRow("Open", "Builds may reach any host.", switchInput({ id: "network-open", name: "mode", label: "Open", on: setting.mode === "open", attrs: { value: "open", "aria-label": "Open: builds may reach any host" } }), "inline"),
132
+ note("Tasks scoped from now on use this; tasks already approved keep the network they were approved with."),
133
+ ]) }))]) });
152
134
  }
153
135
  /** Live view: hidden by default; showing commands takes the password, hiding them is one step. */
154
136
  function liveViewHtml(view) {
@@ -156,63 +138,79 @@ function liveViewHtml(view) {
156
138
  if (live === undefined)
157
139
  return "";
158
140
  const said = live.commands
159
- ? html `<p>Commands and their output show in the live view of this project's agents. Secrets are hidden where they're recognised, which isn't guaranteed.</p>`
160
- : html `<p>Commands are hidden in the live view. You see which files an agent reads and edits, but not the commands it runs or their output.</p>`;
161
- const change = !live.canChange ? html `<p class="meta">An approver for this project can change this.</p>`
141
+ ? "Commands and their output show in the live view of this project's agents. Secrets are hidden where they're recognised, which isn't guaranteed."
142
+ : "Commands are hidden in the live view. You see which files an agent reads and edits, but not the commands it runs or their output.";
143
+ const change = !live.canChange ? APPROVER_CHANGES
162
144
  : live.commands
163
- ? postForm("/settings/project/live-view", html `<button type="submit">Hide commands</button><p class="meta">From the next run.</p>`, { hidden: { repo: view.repo, act: "off" } })
164
- : postForm("/settings/project/live-view", html `<p class="meta">Output can include secrets the filter misses. Turn this on only where you're happy for anyone who can see this project to read it. From the next run.</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Show commands</button>`, { hidden: { repo: view.repo, act: "on", nonce: live.nonce ?? "" } });
165
- return html `<section class="build-access live-view" aria-labelledby="live-view-title" data-live-view-setting="${live.commands ? "commands" : "base"}"><h2 id="live-view-title">Live view</h2>${said}${change}</section>`;
145
+ ? postForm("/settings/project/live-view", joinHtml([note("From the next run."), acts(btn("Hide commands", { sm: true, type: "submit" }))]), { hidden: { repo: view.repo, act: "off" } })
146
+ : kitSectionForm({ proof: "project-live-view", action: "/settings/project/live-view", id: "live-view-form", hidden: { repo: view.repo, act: "on", nonce: live.nonce ?? "" },
147
+ save: { label: "Show commands", variant: "outline" }, confirm: "Show this project's commands in the live view",
148
+ body: note("Output can include secrets the filter misses. Turn this on only where you're happy for anyone who can see this project to read it. From the next run.") });
149
+ return settingsSection({ id: "live-view", title: "Live view", state: html `<span data-live-view-setting="${live.commands ? "commands" : "base"}"> · ${live.commands ? "Commands shown" : "Commands hidden"}</span>`,
150
+ lead: said, body: change });
166
151
  }
167
152
  /** Build access: one sentence for what a build gets, the detail on demand, and the way to change it. */
168
153
  function accessHtml(view) {
169
154
  const access = view.access;
170
155
  if (access === undefined)
171
156
  return "";
172
- const manage = html `<a class="manage-tools" href="/settings/tools?repo=${encodeURIComponent(view.repo)}">Manage tools</a>`;
157
+ const manage = acts(btn("Manage tools", { sm: true, href: `/settings/tools?repo=${encodeURIComponent(view.repo)}` }));
173
158
  const always = [
174
159
  "Your other projects, keys and sign-in files are out of reach.",
175
160
  "Your own Claude and Codex hooks, plugins and MCP servers don't load.",
176
161
  "Builds sign in with your existing Claude or Codex login.",
177
162
  ];
178
163
  if ("problem" in access) {
179
- return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2><p class="problem" role="alert">${access.problem}</p><details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}</ul></details>${manage}</section>`;
164
+ return settingsSection({ id: "build-access", title: "Build access", body: joinHtml([notice(access.problem), disclose("Details", list(always)), manage]) });
180
165
  }
181
166
  const loading = access.tools.filter(one => one.loads);
182
- const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}<span class="tool-name">${one.name}</span>`);
167
+ const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}${mono(one.name)}`);
183
168
  const outcome = loading.length === 0
184
- ? html `<p>Builds work in their task's folder, with no extra tools.</p>`
185
- : html `<p>Builds work in their task's folder, with ${loading.length === 1 ? "one tool" : `${loading.length} tools`}: ${names}.</p>`;
186
- const tools = access.tools.map(one => html `<li><span class="tool-name">${one.name}</span> <span class="meta">· ${one.words}</span></li>`);
187
- return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2>${outcome}<details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}${tools}</ul></details>${manage}</section>`;
169
+ ? html `Builds work in their task's folder, with no extra tools.`
170
+ : html `Builds work in their task's folder, with ${loading.length === 1 ? "one tool" : `${loading.length} tools`}: ${names}.`;
171
+ const tools = access.tools.map(one => html `${mono(one.name)} · ${one.words}`);
172
+ return settingsSection({ id: "build-access", title: "Build access", lead: outcome, body: joinHtml([disclose("Details", list([...always, ...tools])), manage]) });
188
173
  }
189
174
  /** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
190
175
  function buildsHtml(view) {
191
176
  const b = view.builds;
192
177
  if (b === undefined)
193
178
  return "";
194
- const capped = b.capacity !== null && b.capacity < b.setting ? ` Its worker runs ${b.capacity} at once, so ${b.capacity} for now.` : "";
179
+ const capped = b.capacity !== null && b.capacity < b.setting ? ` Its builder runs ${b.capacity} at once, so ${b.capacity} for now.` : "";
195
180
  const now = `${b.building} building now.${capped}`;
196
181
  if (!b.canChange)
197
- return html `<section class="builds-at-once"><h2>Builds at once</h2><p>Up to ${b.setting}. ${now}</p></section>`;
198
- return html `<section class="builds-at-once"><h2>Builds at once</h2>${postForm("/settings/project/concurrency", html `<div class="builds-row"><input type="number" name="concurrency" min="1" max="64" step="1" inputmode="numeric" value="${b.setting}" aria-label="Builds at once" aria-describedby="builds-now"><button type="submit">Save</button></div><p class="meta" id="builds-now">${now}</p>`, { hidden: { repo: view.repo } })}</section>`;
182
+ return settingsSection({ id: "builds-at-once", title: "Builds at once", state: ` · Up to ${b.setting}`, body: note(now) });
183
+ return settingsSection({ id: "builds-at-once", title: "Builds at once",
184
+ body: postForm("/settings/project/concurrency", formRow("Builds at once", now, html `${input({ id: "builds-at-once-n", name: "concurrency", type: "number", value: String(b.setting),
185
+ attrs: { min: 1, max: 64, step: 1, inputmode: "numeric", "aria-label": "Builds at once" } })}${btn("Save", { sm: true, type: "submit" })}`, "inline"), { hidden: { repo: view.repo } }) });
199
186
  }
200
- const note = (notice) => notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
201
- /** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
202
- export function projectSettingsHtml(view, notice) {
203
- const held = html `<p>Toolroll holds ${holdingsWords(view.holdings)} for <strong>${view.name}</strong>.</p>`;
204
- const remote = html `<section class="build-access recorded-remote" aria-labelledby="recorded-remote-title"><h2 id="recorded-remote-title">Recorded remote</h2>${view.remoteUrl
205
- ? html `<p class="path">${view.remoteUrl}</p>`
206
- : html `<p>No remote recorded. Landed work waits for your review.</p>`}<details><summary>Change remote</summary><p>An owner or instance operator can run:</p><p class="path">toolroll repos add &lt;project&gt; --remote &lt;url&gt;</p></details></section>`;
207
- const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${remote}${buildsHtml(view)}${accessHtml(view)}${networkHtml(view)}${writesHtml(view)}${sandboxHtml(view)}${checkAccessHtml(view)}${liveViewHtml(view)}`;
187
+ /** The project's page: its name and path, what Toolroll holds for it, its build settings, and (for an instance operator)
188
+ * Remove and Delete. */
189
+ export function projectSettingsHtml(view, said) {
190
+ const answer = said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "";
191
+ const project = settingsSection({ id: "project", title: view.name, lead: mono(view.repo),
192
+ body: joinHtml([
193
+ note(`Toolroll holds ${holdingsWords(view.holdings)} for this project.`),
194
+ formRow("Recorded remote", view.remoteUrl ? null : "None. Landed work waits for your review.", view.remoteUrl ? mono(view.remoteUrl) : html ``, view.remoteUrl ? "wide" : "inline"),
195
+ disclose("Change remote", joinHtml([note("An owner or instance operator can run:"), codeLine("toolroll repos add <project> --remote <url>")])),
196
+ ]) });
197
+ const settings = joinHtml([buildsHtml(view), accessHtml(view), networkHtml(view), writesHtml(view), sandboxHtml(view), checkAccessHtml(view), liveViewHtml(view)]);
208
198
  if (!view.canDelete)
209
- return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
210
- const body = view.running.length > 0
211
- ? html `<p class="problem">Its work is running: ${view.running.join(", ")}. Stop it before deleting the project.</p>`
212
- : html `<p>Removes everything Toolroll holds for ${view.name}. The repository and its own branches stay. There's no undo.</p>${postForm("/settings/project/delete", html `<label><span>Type <strong>${view.name}</strong> to continue</span><input name="name" autocomplete="off" autocapitalize="off" spellcheck="false" required></label><button type="submit">Continue</button>`, { hidden: { repo: view.repo } })}`;
199
+ return joinHtml([answer, project, settings, note("An instance operator can remove or delete a project.")]);
200
+ const deleteBody = view.running.length > 0
201
+ ? notice(`Its work is running: ${view.running.join(", ")}. Stop it before deleting the project.`)
202
+ : joinHtml([note(`Removes everything Toolroll holds for ${view.name}. The repository and its own branches stay. There's no undo.`),
203
+ postForm("/settings/project/delete", joinHtml([
204
+ formRow(`Type ${view.name} to continue`, null, input({ id: "delete-name", name: "name", attrs: { autocomplete: "off", autocapitalize: "off", spellcheck: "false", required: true, "aria-label": `Type ${view.name} to continue` } })),
205
+ acts(btn("Continue", { sm: true, type: "submit", variant: "danger" })),
206
+ ]), { hidden: { repo: view.repo } })]);
213
207
  // Removing is the reversible step: off the lists and the builder, everything kept.
214
- const remove = postForm("/projects/remove", html `<button type="submit">Remove from Toolroll</button><p class="meta">Its tasks and results stay saved. Add it again to bring it back.</p>`, { attrs: { class: "remove-project" }, hidden: { repo: view.repo } });
215
- return html `<section class="project-settings">${head}${remove}<details class="danger-zone"${notice.problem ? html ` open` : ""}><summary>Delete project</summary>${body}</details></section>`;
208
+ const leave = settingsSection({ id: "remove", title: "Remove or delete",
209
+ body: joinHtml([
210
+ formRow("Remove from Toolroll", "Its tasks and results stay saved. Add it again to bring it back.", postForm("/projects/remove", btn("Remove from Toolroll", { sm: true, type: "submit" }), { hidden: { repo: view.repo } }), "inline"),
211
+ disclose("Delete project", deleteBody, Boolean(said.problem)),
212
+ ]) });
213
+ return joinHtml([answer, project, settings, leave]);
216
214
  }
217
215
  /** The second step: exactly what goes, and the password. */
218
216
  export function projectDeleteConfirmHtml(view, problem) {
@@ -226,5 +224,16 @@ export function projectDeleteConfirmHtml(view, problem) {
226
224
  ...(h.subagents > 0 ? [count(h.subagents, "subagent")] : []),
227
225
  "Its budgets, settings and knowledge",
228
226
  ];
229
- return html `<section class="project-delete">${problem === null ? "" : html `<p class="problem" role="alert">${problem}</p>`}<p>This removes:</p><ul class="removes">${items.map(one => html `<li>${one}</li>`)}</ul><p>The repository at <span class="path">${view.repo}</span> and its own branches stay. The ledger keeps its history. There's no undo.</p>${postForm("/settings/project/delete", html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}<div class="actions"><button type="submit" class="danger">Delete project</button><a href="/settings/project?repo=${encodeURIComponent(view.repo)}">Cancel</a></div>`, { hidden: { repo: view.repo, name: view.name, step: "delete" } })}</section>`;
227
+ return joinHtml([
228
+ problem === null ? "" : notice(problem),
229
+ settingsSection({ id: "delete", title: "This removes",
230
+ body: joinHtml([
231
+ list(items),
232
+ note(html `The repository at ${mono(view.repo)} and its own branches stay. The ledger keeps its history. There's no undo.`),
233
+ // The answer is a page of its own (this page again with a refusal, or the project page), so it posts as a navigation.
234
+ kitSectionForm({ proof: "project-delete", action: "/settings/project/delete", id: "delete-form", native: true, hidden: { repo: view.repo, name: view.name, step: "delete" },
235
+ save: { label: "Delete project", variant: "danger" }, confirm: `Delete ${view.name}`,
236
+ body: acts(btn("Cancel", { sm: true, variant: "ghost", href: `/settings/project?repo=${encodeURIComponent(view.repo)}` })) }),
237
+ ]) }),
238
+ ]);
230
239
  }
@@ -86,7 +86,9 @@ export function projectRunning(store, repo, now) {
86
86
  const chats = count("SELECT COUNT(*) AS n FROM mate_turn WHERE state IN ('queued','running') AND id IN (SELECT value FROM json_each(?))", json(d.turns));
87
87
  const steps = count("SELECT COUNT(*) AS n FROM flow_step_run WHERE state = 'running' AND card IN (SELECT value FROM json_each(?))", json(d.cards));
88
88
  const calls = count("SELECT COUNT(*) AS n FROM subagent_call WHERE state = 'running' AND subagent IN (SELECT value FROM json_each(?))", json(d.subagents));
89
- const merges = count(`SELECT COUNT(*) AS n FROM merge_intent WHERE state IN ('claimed','firing') AND publication IN (SELECT id FROM publication WHERE task_ref IN (SELECT value FROM json_each(?)))`, json(d.refs));
89
+ const merges = count(`SELECT COUNT(*) AS n FROM merge_intent WHERE state IN ('claimed','firing') AND publication IN (SELECT id FROM publication WHERE task_ref IN (SELECT value FROM json_each(?)))`, json(d.refs))
90
+ // A person's merge between its last check and GitHub's answer (v145); a row older than 15 minutes is a crash.
91
+ + count("SELECT COUNT(*) AS n FROM merge_in_flight WHERE at > ? AND task_ref IN (SELECT value FROM json_each(?))", new Date(now.getTime() - 15 * 60_000).toISOString(), json(d.refs));
90
92
  const checkouts = count("SELECT COUNT(*) AS n FROM worktree WHERE repo = ? AND runner IS NOT NULL AND released_at IS NULL", repo);
91
93
  return [
92
94
  ...(building > 0 ? [`${plural(building, "task")} ${building === 1 ? "is" : "are"} being built`] : []),
@@ -267,6 +269,8 @@ function deleteRows(store, repo, d, now) {
267
269
  del("skill_test", `${IN("task_ref")} OR ${IN("source_run")}`, T, R);
268
270
  del("merge_blocker", `${IN("publication")} OR ${IN("task_id")}`, publications, K);
269
271
  del("merge_intent", IN("publication"), publications);
272
+ del("merge_in_flight", IN("publication"), publications);
273
+ del("deploy_in_flight", IN("task_ref"), T);
270
274
  del("publication", IN("id"), publications);
271
275
  del("artifact", IN("id"), A);
272
276
  // Tasks and their versions.
@@ -282,7 +286,7 @@ function deleteRows(store, repo, d, now) {
282
286
  del("workflow_preview", "repo = ?", repo);
283
287
  del("coding_handoff_scope", `id IN (SELECT id FROM coding_handoff WHERE json_extract(payload, '$.repo') = ?)`, repo);
284
288
  del("coding_handoff", "json_extract(payload, '$.repo') = ?", repo);
285
- for (const table of ["task_act", "task_replacement"])
289
+ for (const table of ["task_act", "task_replacement", "task_archive"])
286
290
  del(table, IN("task_ref"), T);
287
291
  del("task_ref", IN("id"), T);
288
292
  del("task_scope", IN("task_id"), K);
@@ -293,7 +297,7 @@ function deleteRows(store, repo, d, now) {
293
297
  del("flow_trigger", IN("flow"), F);
294
298
  del("flow_unattended_run", `grant_id IN (SELECT id FROM flow_unattended_grant WHERE ${IN("flow")})`, F);
295
299
  del("flow_unattended_grant", IN("flow"), F);
296
- for (const table of ["flow_card_watcher", "flow_comment", "flow_event", "flow_mail", "flow_step_run"])
300
+ for (const table of ["flow_card_watcher", "flow_comment", "flow_event", "flow_mail", "flow_step_run", "flow_step_task"])
297
301
  del(table, IN("card"), C);
298
302
  del("subagent_question", IN("id"), questions);
299
303
  del("subagent_call", `${IN("subagent")} OR ${IN("card")}`, M, C);
@@ -22,19 +22,19 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
- title?: never;
26
- content?: never;
25
+ history?: never;
27
26
  revision: number;
28
27
  stale: boolean;
29
28
  notice: string;
30
29
  decision?: never;
31
- history?: never;
30
+ title?: never;
31
+ content?: never;
32
32
  instructions?: never;
33
33
  references?: never;
34
34
  decisions?: never;
35
35
  } | {
36
- title?: never;
37
- content?: never;
36
+ revision?: never;
37
+ history?: never;
38
38
  stale?: never;
39
39
  decision: {
40
40
  repo: string;
@@ -51,27 +51,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
51
51
  id: number;
52
52
  };
53
53
  notice: string;
54
- revision?: never;
55
- history?: never;
54
+ title?: never;
55
+ content?: never;
56
56
  instructions?: never;
57
57
  references?: never;
58
58
  decisions?: never;
59
59
  } | {
60
+ history?: never;
60
61
  stale?: never;
61
62
  decision?: never;
62
63
  revision: number;
63
64
  title: string;
64
65
  content: string;
65
66
  notice: string;
66
- history?: never;
67
67
  instructions?: never;
68
68
  references?: never;
69
69
  decisions?: never;
70
70
  } | {
71
- title?: never;
72
- content?: never;
73
71
  stale?: never;
74
72
  decision?: never;
73
+ title?: never;
74
+ content?: never;
75
75
  revision: number;
76
76
  history: {
77
77
  revision: number;
package/dist/publish.d.ts CHANGED
@@ -131,3 +131,14 @@ export declare function sweepMerges(store: Store, options: {
131
131
  clock?: () => Date;
132
132
  exec?: PublishExec;
133
133
  }): Promise<MergeReport>;
134
+ /** A conditional settle (store.settleObservation) and, only when it won, that head's checks' own timing (v146 ci_check),
135
+ * in one transaction: a stale, losing answer neither settles nor records a check. */
136
+ export declare function settleWithChecks(store: Store, observation: {
137
+ githubRepo: string;
138
+ prNumber: number;
139
+ headSha: string;
140
+ state: "passing" | "failing" | "running" | "none";
141
+ generation: number;
142
+ }, rollup: unknown, now: Date): {
143
+ won: boolean;
144
+ };
package/dist/publish.js CHANGED
@@ -26,7 +26,7 @@ import { tmpdir } from "node:os";
26
26
  import { join } from "node:path";
27
27
  import { run as execRun } from "./exec.js";
28
28
  import { summarizeChecks } from "./pulls.js";
29
- import { releasePublication } from "./store.js";
29
+ import { parseCiRollup, recordCiChecks, releasePublication } from "./store.js";
30
30
  import { headWithin } from "./names.js";
31
31
  export const BODY_TEMPLATE_VERSION = 2;
32
32
  /** After this many failed attempts a publication stops retrying and pages instead. */
@@ -340,6 +340,8 @@ export async function observeChecks(store, options) {
340
340
  if (!settled.won)
341
341
  return;
342
342
  won = true;
343
+ // The checks' own timing (v146), only with the observation that won.
344
+ recordCiChecks(store, publication.githubRepo, publication.prNumber, headOid, parseCiRollup(payload.statusCheckRollup), clock());
343
345
  store.recordPublicationRemoteState(publication.id, remoteState, clock());
344
346
  report.resolved += store.resolveCiEpisodes(publication.githubRepo, publication.prNumber, null, clock());
345
347
  // A closed PR moots its merge blocker and supersedes its intent —
@@ -361,7 +363,8 @@ export async function observeChecks(store, options) {
361
363
  continue;
362
364
  }
363
365
  const state = summarizeChecks(payload.statusCheckRollup);
364
- const settled = store.settleObservation({ githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state, generation }, clock());
366
+ // Settled and, when it won, the checks' own timing recorded (v146) in one transaction: a losing generation writes nothing.
367
+ const settled = settleWithChecks(store, { githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state, generation }, payload.statusCheckRollup, clock());
365
368
  if (!settled.won) {
366
369
  // An out-of-order response: a newer observation already spoke.
367
370
  // Nothing persists from this one (round-4 findings 24/25).
@@ -465,7 +468,7 @@ async function observeOne(store, exec, clock, githubRepo, prNumber) {
465
468
  if (String(payload.state ?? "").toUpperCase() !== "OPEN")
466
469
  return { ok: false, why: "pr-" + String(payload.state ?? "gone").toLowerCase() };
467
470
  const state = summarizeChecks(payload.statusCheckRollup);
468
- const settled = store.settleObservation({ githubRepo, prNumber, headSha, state, generation }, clock());
471
+ const settled = settleWithChecks(store, { githubRepo, prNumber, headSha, state, generation }, payload.statusCheckRollup, clock());
469
472
  // A losing settle is NO observation (round-4 finding 24).
470
473
  if (!settled.won)
471
474
  return { ok: false, why: "stale-observation" };
@@ -707,3 +710,13 @@ export async function sweepMerges(store, options) {
707
710
  }
708
711
  return report;
709
712
  }
713
+ /** A conditional settle (store.settleObservation) and, only when it won, that head's checks' own timing (v146 ci_check),
714
+ * in one transaction: a stale, losing answer neither settles nor records a check. */
715
+ export function settleWithChecks(store, observation, rollup, now) {
716
+ return store.transact(() => {
717
+ const settled = store.settleObservation(observation, now);
718
+ if (settled.won)
719
+ recordCiChecks(store, observation.githubRepo, observation.prNumber, observation.headSha, parseCiRollup(rollup), now);
720
+ return settled;
721
+ });
722
+ }