toolroll 0.9.63 → 0.9.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
package/dist/exec.js CHANGED
@@ -6,9 +6,11 @@
6
6
  * command. And nothing here rejects: a failed command is a value. Discovery
7
7
  * walks repos it has never seen, and one broken repo must not end the scan.
8
8
  */
9
+ import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
9
10
  import { linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced } from "./agent-fence.js";
10
- import { execFile, spawn } from "node:child_process";
11
- import { realpathSync } from "node:fs";
11
+ import { execFile, execFileSync, spawn } from "node:child_process";
12
+ import { readFileSync, realpathSync } from "node:fs";
13
+ import { join } from "node:path";
12
14
  import { observeProcessTree, stopProcessTree } from "./process-tree.js";
13
15
  import { jsonlDiscriminants } from "./jsonl-discriminants.js";
14
16
  import { createContainer, currentContainment } from "./containment.js";
@@ -45,6 +47,24 @@ const containers = new WeakMap();
45
47
  export class ContainmentRefusal extends Error {
46
48
  name = "ContainmentRefusal";
47
49
  }
50
+ /**
51
+ * The command as it runs inside the agent fence (sandbox-exec or bubblewrap wrapping it), or as it is when it has no
52
+ * fence. A spawn whose confinement is required and that no backend here can wrap is refused (ContainmentRefusal):
53
+ * nothing runs, and nothing falls back to running it unwrapped.
54
+ */
55
+ export function confined(file, args, bag) {
56
+ const fence = bag.fence ?? [];
57
+ if (fence.length > 0 && macosFenceAvailable())
58
+ return macosFenced(file, args, fence, bag.fenceGrants);
59
+ if (fence.length > 0 && linuxFenceAvailable())
60
+ return linuxFenced(file, args, fence, bag.fenceGrants);
61
+ if (bag.confinement === "required") {
62
+ throw new ContainmentRefusal(fence.length === 0
63
+ ? "This process must run inside the agent fence, and it was given none."
64
+ : "This machine can't confine a build (no macOS sandbox, no working bubblewrap), so nothing was started.");
65
+ }
66
+ return { file, args: [...args] };
67
+ }
48
68
  /** A spawn callback refused custody AFTER the child existed: the caller
49
69
  * must reap exactly this child before answering. */
50
70
  class SpawnCustodyFailure extends Error {
@@ -72,10 +92,7 @@ function rejectedChild(error, child) {
72
92
  */
73
93
  function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
74
94
  // The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
75
- if (bag.fence !== undefined && bag.fence.length > 0 && macosFenceAvailable())
76
- ({ file, args } = macosFenced(file, args, bag.fence, bag.fenceGrants));
77
- else if (bag.fence !== undefined && bag.fence.length > 0 && linuxFenceAvailable())
78
- ({ file, args } = linuxFenced(file, args, bag.fence, bag.fenceGrants));
95
+ ({ file, args } = confined(file, args, bag));
79
96
  // The policy refusal comes first: nothing is reserved, recorded or
80
97
  // spawned for a spawn that cannot be contained as required.
81
98
  const effective = contain ? currentContainment() : null;
@@ -224,7 +241,7 @@ function containmentOf(container, empty, attachFailure) {
224
241
  }
225
242
  /** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
226
243
  function spawnStream(file, args, options, stdio, childEnv) {
227
- return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence, fenceGrants: options.fenceGrants }, options.processGroup === true);
244
+ return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence, fenceGrants: options.fenceGrants, confinement: options.confinement }, options.processGroup === true);
228
245
  }
229
246
  /** The refusal-before-spawn shape shared by every containable transport. */
230
247
  function refusedResult(error) {
@@ -522,6 +539,7 @@ export function run(file, args, options = {}) {
522
539
  ...(options.onStderr === undefined ? {} : { onStderr: options.onStderr }),
523
540
  ...(options.fence === undefined ? {} : { fence: options.fence }),
524
541
  ...(options.fenceGrants === undefined ? {} : { fenceGrants: options.fenceGrants }),
542
+ ...(options.confinement === undefined ? {} : { confinement: options.confinement }),
525
543
  ...(options.onDescendant === undefined ? {} : { onDescendant: options.onDescendant }),
526
544
  ...(options.onDescendantWriteFailure === undefined ? {} : { onDescendantWriteFailure: options.onDescendantWriteFailure }),
527
545
  ...(options.onDescendantExit === undefined ? {} : { onDescendantExit: options.onDescendantExit }),
@@ -531,8 +549,19 @@ export function run(file, args, options = {}) {
531
549
  ...(options.onContainerEmpty === undefined ? {} : { onContainerEmpty: options.onContainerEmpty }),
532
550
  }));
533
551
  }
552
+ // The plain road wraps exactly as the process-group road does: a fence given here is never dropped, and a spawn
553
+ // whose confinement is required and can't be wrapped refuses before anything runs.
554
+ let wrapped;
555
+ try {
556
+ wrapped = confined(file, args, options);
557
+ }
558
+ catch (error) {
559
+ if (!(error instanceof ContainmentRefusal))
560
+ throw error;
561
+ return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: error.message, timedOut: false, notFound: false, containment: { refused: error.message } });
562
+ }
534
563
  return retryTransientSpawn(() => new Promise(resolve => {
535
- execFile(file, [...args], {
564
+ execFile(wrapped.file, wrapped.args, {
536
565
  cwd,
537
566
  timeout: timeoutMs,
538
567
  maxBuffer,
@@ -564,7 +593,7 @@ function runBufferedGroup(file, args, bag) {
564
593
  let container = null;
565
594
  let attached;
566
595
  try {
567
- const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence, fenceGrants: bag.fenceGrants });
596
+ const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence, fenceGrants: bag.fenceGrants, confinement: bag.confinement });
568
597
  child = spawned.child;
569
598
  container = spawned.container;
570
599
  attached = spawned.attached;
@@ -1403,6 +1432,335 @@ export function runGeminiStreamJsonl(file, args, options = {}) {
1403
1432
  child.on("close", code => finish(code));
1404
1433
  });
1405
1434
  }
1435
+ /** One process listing: ids, group, state and command name only (never arguments — they can carry a brief). */
1436
+ function processRows() {
1437
+ try {
1438
+ return execFileSync("/bin/ps", ["-axo", "pid=,ppid=,pgid=,stat=,comm="], { encoding: "utf8", timeout: 2_000, maxBuffer: 8 * 1024 * 1024 })
1439
+ .split("\n").flatMap(line => {
1440
+ const parts = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
1441
+ return parts === null ? [] : [{ pid: Number(parts[1]), parent: Number(parts[2]), group: Number(parts[3]), state: parts[4], name: parts[5].trim() }];
1442
+ });
1443
+ }
1444
+ catch {
1445
+ return null;
1446
+ }
1447
+ }
1448
+ /**
1449
+ * What the agent started and is still running: from `root` (the spawned process — the agent itself, or a fence
1450
+ * wrapper around it), down to the agent's own process (the first whose command is the agent's launch, `names`), then
1451
+ * every live, non-zombie process beneath it, with its group. The agent itself and any wrapper are never counted.
1452
+ */
1453
+ function liveUnderAgent(root, names) {
1454
+ const rows = processRows();
1455
+ if (rows === null)
1456
+ return null;
1457
+ const children = new Map();
1458
+ for (const row of rows)
1459
+ children.set(row.parent, [...(children.get(row.parent) ?? []), row]);
1460
+ const self = rows.find(row => row.pid === root);
1461
+ const named = (row) => names.some(name => row.name === name || row.name.endsWith(`/${name}`));
1462
+ // The agent: the root when it is the agent (macOS sandbox-exec execs in place), else the first named descendant.
1463
+ let agent = self !== undefined && named(self) ? root : null;
1464
+ const pending = [...(children.get(root) ?? [])];
1465
+ while (agent === null && pending.length > 0) {
1466
+ const row = pending.shift();
1467
+ if (named(row))
1468
+ agent = row.pid;
1469
+ else
1470
+ pending.push(...(children.get(row.pid) ?? []));
1471
+ }
1472
+ if (agent === null)
1473
+ return [];
1474
+ const found = [];
1475
+ const below = [...(children.get(agent) ?? [])];
1476
+ while (below.length > 0) {
1477
+ const row = below.pop();
1478
+ if (!row.state.startsWith("Z"))
1479
+ found.push({ pid: row.pid, group: row.group });
1480
+ below.push(...(children.get(row.pid) ?? []));
1481
+ }
1482
+ return found;
1483
+ }
1484
+ /** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
1485
+ export const ACP_SHUTDOWN_MS = { close: 2_000, eof: 5_000, term: 5_000 };
1486
+ /** How long each handshake step (initialize, session/new, choosing the model) may take before the agent is stopped. */
1487
+ export const ACP_HANDSHAKE_MS = 30_000;
1488
+ /** After the agent itself has exited, how long its pipes may stay open (a descendant holding them) before they are closed. */
1489
+ const ACP_PIPES_AFTER_EXIT_MS = 5_000;
1490
+ /** SIGTERM the child's process group (POSIX) or the child alone; a container's own stop is the forced one, later. */
1491
+ function termGroup(child) {
1492
+ const pid = child.pid;
1493
+ try {
1494
+ if (pid !== undefined && process.platform !== "win32")
1495
+ process.kill(-pid, "SIGTERM");
1496
+ else
1497
+ child.kill("SIGTERM");
1498
+ }
1499
+ catch {
1500
+ // Already gone.
1501
+ }
1502
+ }
1503
+ /**
1504
+ * One ACP build conversation over the agent's stdio (docs/plans/acp-adapter.md § Gateway, runner, custody). The
1505
+ * conversation itself is acp-client.ts; this owns the process: the same contained spawn, ownership, descendant
1506
+ * observation, container settlement and watchdog every other runner uses, with both clocks at once.
1507
+ *
1508
+ * After the prompt is answered it shuts the agent down in bounded steps — session/close (when offered), stdin EOF,
1509
+ * SIGTERM to the group, then the forced kill — and returns only when the process has settled, so the gateway's
1510
+ * witness records the real exit. A protocol failure kills at once. A stop, from the gateway's stop watch, kills at
1511
+ * any step. The result's stdout is the conversation's retained record, never the agent's stream; its code is 0 only
1512
+ * when the prompt was answered end_turn and the agent then exited cleanly or on our own signal.
1513
+ */
1514
+ export function runAcpSession(file, args, options = {}) {
1515
+ const acp = options.acp;
1516
+ if (acp === undefined)
1517
+ return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: "an ACP run needs its conversation terms", timedOut: false, notFound: false });
1518
+ const childEnv = resolveChildEnv(options);
1519
+ return new Promise(resolve => {
1520
+ let child;
1521
+ let container = null;
1522
+ let attachFailure = null;
1523
+ try {
1524
+ const spawned = spawnStream(file, args, options, ["pipe", "pipe", "pipe"], childEnv);
1525
+ child = spawned.child;
1526
+ container = spawned.container;
1527
+ void spawned.attached.then(outcome => { if (!outcome.ok)
1528
+ attachFailure = outcome.detail; });
1529
+ }
1530
+ catch (error) {
1531
+ void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
1532
+ return;
1533
+ }
1534
+ if (options.processGroup === true)
1535
+ liveProviders.add(child);
1536
+ registerOwned(options.owner, child);
1537
+ if (options.processGroup === true && container === null)
1538
+ observeProcessTree(child, options);
1539
+ const conversation = createAcpConversation({ cwd: options.cwd ?? process.cwd(), prompt: acp.prompt, model: acp.model, expect: acp.expect });
1540
+ // The agent's own process, as a listing names it: the launched file, or the script a pinned interpreter runs.
1541
+ const agentNames = [...new Set([file, ...args.slice(0, 2)].filter(one => one.startsWith("/")).map(one => one.split("/").pop()).filter(one => one !== ""))];
1542
+ let stderr = "";
1543
+ let timedOut = false;
1544
+ let notFound = false;
1545
+ let settled = false;
1546
+ let shuttingDown = false;
1547
+ /** The signals the plane itself sent after the prompt was answered, by its own shutdown ladder: an exit by one of
1548
+ * those is clean. A stop or a timeout kills through other roads and never sets these. */
1549
+ let termAfterTerminal = false;
1550
+ let killAfterTerminal = false;
1551
+ /** How the process came to end, for the record: on its own (before or after stdin closed), or by our signal. */
1552
+ let endedBy = "self";
1553
+ let announced = null;
1554
+ const timers = [];
1555
+ const later = (ms, fn) => { const timer = setTimeout(fn, ms); timer.unref?.(); timers.push(timer); };
1556
+ const write = (lines) => {
1557
+ for (const one of lines) {
1558
+ if (child.stdin === null || child.stdin.destroyed || child.stdin.writableEnded)
1559
+ return;
1560
+ try {
1561
+ child.stdin.write(`${one}\n`);
1562
+ }
1563
+ catch { /* the agent closed its stdin: its exit decides */ }
1564
+ }
1565
+ };
1566
+ const forceKill = () => {
1567
+ endedBy = "kill";
1568
+ if (options.processGroup === true)
1569
+ killGroup(child);
1570
+ else
1571
+ child.kill("SIGKILL");
1572
+ };
1573
+ /** Each handshake step has its own deadline, beside the run's clocks. */
1574
+ let handshakePhase = null;
1575
+ const armHandshake = () => {
1576
+ const phase = conversation.phase();
1577
+ if (phase === handshakePhase || (phase !== "initialize" && phase !== "session" && phase !== "model"))
1578
+ return;
1579
+ handshakePhase = phase;
1580
+ const limit = acp.handshakeMs ?? ACP_HANDSHAKE_MS;
1581
+ later(limit, () => {
1582
+ if (settled || conversation.phase() !== phase)
1583
+ return;
1584
+ conversation.refuse(`the agent took more than ${limit / 1000} s to answer ${phase === "initialize" ? "initialize" : phase === "session" ? "session/new" : "the model choice"}`);
1585
+ forceKill();
1586
+ });
1587
+ };
1588
+ const exited = () => child.exitCode !== null || child.signalCode !== null;
1589
+ const shutdown = () => {
1590
+ if (shuttingDown)
1591
+ return;
1592
+ shuttingDown = true;
1593
+ if (conversation.phase() === "failed") {
1594
+ forceKill();
1595
+ return;
1596
+ }
1597
+ const endInput = () => {
1598
+ if (exited())
1599
+ return;
1600
+ try {
1601
+ child.stdin?.end();
1602
+ }
1603
+ catch { /* gone */ }
1604
+ endedBy = "eof";
1605
+ later(ACP_SHUTDOWN_MS.eof, () => {
1606
+ if (exited())
1607
+ return;
1608
+ termAfterTerminal = conversation.phase() === "ended";
1609
+ endedBy = "term";
1610
+ termGroup(child);
1611
+ later(ACP_SHUTDOWN_MS.term, () => {
1612
+ if (exited())
1613
+ return;
1614
+ killAfterTerminal = conversation.phase() === "ended";
1615
+ forceKill();
1616
+ });
1617
+ });
1618
+ };
1619
+ const close = conversation.close();
1620
+ if (close.length === 0) {
1621
+ endInput();
1622
+ return;
1623
+ }
1624
+ write(close);
1625
+ const started = Date.now();
1626
+ const waitClose = () => {
1627
+ if (conversation.closeAnswered() || Date.now() - started >= ACP_SHUTDOWN_MS.close || exited())
1628
+ endInput();
1629
+ else
1630
+ later(50, waitClose);
1631
+ };
1632
+ waitClose();
1633
+ };
1634
+ const onLine = (framed) => {
1635
+ if (settled)
1636
+ return;
1637
+ armHandshake();
1638
+ const reaction = framed.overflowed
1639
+ ? (conversation.refuse("the agent wrote a line too long to read"), { send: [], live: [] })
1640
+ : conversation.receive(framed.prefix.toString("utf8"));
1641
+ write(reaction.send);
1642
+ const id = conversation.sessionId();
1643
+ if (id !== null && announced === null) {
1644
+ announced = id;
1645
+ try {
1646
+ options.onSessionId?.(id);
1647
+ }
1648
+ catch { /* a registry that cannot be written must not kill the turn */ }
1649
+ }
1650
+ for (const update of reaction.live) {
1651
+ try {
1652
+ options.onStreamEvent?.({ acp: update });
1653
+ }
1654
+ catch { /* observational only */ }
1655
+ }
1656
+ const phase = conversation.phase();
1657
+ if (phase === "failed" || phase === "ended")
1658
+ shutdown();
1659
+ else
1660
+ armHandshake();
1661
+ };
1662
+ const framing = boundedJsonlFramer(ACP_LINE_CAP, onLine);
1663
+ const watchdog = streamWatchdog(options, () => {
1664
+ timedOut = true;
1665
+ forceKill();
1666
+ });
1667
+ child.stdout?.on("data", (chunk) => {
1668
+ watchdog.touch();
1669
+ framing.push(chunk);
1670
+ });
1671
+ child.stderr?.setEncoding("utf8");
1672
+ child.stderr?.on("data", (chunk) => {
1673
+ watchdog.touch();
1674
+ if (stderr.length < JSONL_STDERR_CAP)
1675
+ stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
1676
+ });
1677
+ child.stdin?.on("error", () => { });
1678
+ write(conversation.start());
1679
+ armHandshake();
1680
+ try {
1681
+ acp.onControl?.({
1682
+ cancel: (deadlineMs) => {
1683
+ write(conversation.cancel());
1684
+ if (deadlineMs !== undefined)
1685
+ later(deadlineMs, () => {
1686
+ if (settled || conversation.terminal() !== null)
1687
+ return;
1688
+ conversation.refuse(`the agent didn't answer the cancel within ${Math.round(deadlineMs / 1000)} s`);
1689
+ forceKill();
1690
+ });
1691
+ },
1692
+ liveDescendants: () => process.platform === "win32" ? null : exited() || child.pid === undefined ? [] : liveUnderAgent(child.pid, agentNames),
1693
+ });
1694
+ }
1695
+ catch { /* the caller's own failure */ }
1696
+ // The moment the agent itself exits, what it left in its own group ends with it — not at "close", which a
1697
+ // descendant holding the agent's pipes can postpone forever; and if those pipes are still open shortly after, they
1698
+ // are closed, so settlement never waits on a stray. Escaped groups are the custody observer's, as for every runner.
1699
+ let leftover = false;
1700
+ child.on("exit", () => {
1701
+ if (options.processGroup === true && container === null && child.pid !== undefined && process.platform !== "win32") {
1702
+ try {
1703
+ process.kill(-child.pid, 0);
1704
+ leftover = true;
1705
+ }
1706
+ catch {
1707
+ leftover = false;
1708
+ }
1709
+ try {
1710
+ process.kill(-child.pid, "SIGKILL");
1711
+ }
1712
+ catch { /* none left */ }
1713
+ }
1714
+ later(ACP_PIPES_AFTER_EXIT_MS, () => {
1715
+ if (settled)
1716
+ return;
1717
+ for (const pipe of [child.stdout, child.stderr, child.stdin]) {
1718
+ try {
1719
+ pipe?.destroy();
1720
+ }
1721
+ catch { /* gone */ }
1722
+ }
1723
+ });
1724
+ });
1725
+ const finish = (code, signal) => {
1726
+ if (settled)
1727
+ return;
1728
+ // The last record, even one with no newline after it, is read before anything is decided.
1729
+ framing.finish();
1730
+ settled = true;
1731
+ for (const timer of timers)
1732
+ clearTimeout(timer);
1733
+ watchdog.stop();
1734
+ liveProviders.delete(child);
1735
+ // Whatever the agent started in its own group and left running ends with it (a group id can't be reused while any
1736
+ // member lives, so this reaches only what it left). Escaped groups are the custody observer's, as for every runner.
1737
+ const terminal = conversation.terminal();
1738
+ const answered = terminal !== null && "stopReason" in terminal && terminal.stopReason === "end_turn" && conversation.failure() === null;
1739
+ // A clean exit after the answer, or an exit by the plane's own post-answer signal (its shutdown ladder's SIGTERM,
1740
+ // then SIGKILL), is success; anything else — a stop, a timeout, another signal — is its code.
1741
+ const ownSignal = (termAfterTerminal && (signal === "SIGTERM" || code === 143)) || (killAfterTerminal && (signal === "SIGKILL" || code === 137));
1742
+ const exitCode = answered && !timedOut && (code === 0 || ownSignal) ? 0 : (code ?? 1);
1743
+ const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
1744
+ void settlement.then(empty => resolve({
1745
+ code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : exitCode,
1746
+ // The runner's own account of the ending rides last: a type no agent can emit.
1747
+ stdout: `${conversation.retained()}${JSON.stringify({ type: "acp.exit", how: endedBy, leftover: leftover || empty === false, timedOut })}\n`,
1748
+ stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
1749
+ timedOut,
1750
+ notFound,
1751
+ ...containmentOf(container, empty, attachFailure),
1752
+ }));
1753
+ };
1754
+ child.on("error", error => {
1755
+ endedBy = "error";
1756
+ notFound = error.code === "ENOENT";
1757
+ if (stderr === "")
1758
+ stderr = String(error);
1759
+ finish(null, null);
1760
+ });
1761
+ child.on("close", (code, signal) => finish(code, signal));
1762
+ });
1763
+ }
1406
1764
  const TRUNCATION_MARKER = "…[truncated]";
1407
1765
  /**
1408
1766
  * One serialized synthetic line, guaranteed ≤ JSONL_LINE_CAP in UTF-8
package/dist/export-ui.js CHANGED
@@ -2,10 +2,10 @@
2
2
  * Settings → Data (v105): an instance operator downloads everything Standing
3
3
  * Orders knows as one .zip. One form: the password, typed again, and a button.
4
4
  */
5
- import { html, postForm } from "./html.js";
5
+ import { html, postForm, reauth } from "./html.js";
6
6
  export const EXPORT_CSS = `.data-export{max-width:720px;min-width:0}.data-export label{display:grid;gap:4px;margin:14px 0 0;font-size:.875rem}` +
7
7
  `.data-export input[type=password]{width:100%;max-width:360px;box-sizing:border-box}.data-export button[type=submit]{margin-top:14px;min-height:44px;white-space:nowrap}`;
8
8
  export function dataExportHtml(notice) {
9
9
  const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : "";
10
- return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Download export</button>`)}<p class="meta">The same export from the command line: <code>toolroll export --out &lt;path&gt; [--zip]</code></p></section>`;
10
+ return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Download export</button>`)}<p class="meta">The same export from the command line: <code>toolroll export --out &lt;path&gt; [--zip]</code></p></section>`;
11
11
  }
@@ -6,10 +6,10 @@
6
6
  * waits for people. See flows.ts for what each zone does.
7
7
  */
8
8
  import { assignmentOf } from "./assignment.js";
9
- import { filerFor } from "./approval-policy.js";
10
9
  import { diagnoseTaskDispatch } from "./dispatch.js";
11
10
  import { passOn, TEXT_LIMITS } from "./text-limits.js";
12
11
  import { deciderOf, durationWords, fillFlowText, flowGoal, flowValueLabel, flowWorkTemplate, validateFlowDefinition, withinHours } from "./flows.js";
12
+ import { readSnapshot, referenceWords, snapshotOf } from "./task-source.js";
13
13
  import { reportFillIns, reportSummaryFor } from "./report-summary.js";
14
14
  import { fileTaskProposal } from "./proposal.js";
15
15
  import { FLOW_ACCEPTANCE, unattendedCoverageFor } from "./flow-unattended.js";
@@ -273,13 +273,20 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
273
273
  title: (report ? `${stage.title}: ${card.title}` : card.title).slice(0, 200),
274
274
  repo,
275
275
  goal,
276
- filedVia: `flow:${flow.id}`, filedBy: filerFor(card.createdBy), // who made the card asked for the work (an owner can be reassigned by anyone)
276
+ filedVia: `flow:${flow.id}`,
277
+ // Who made the card asked for the work (an owner can be reassigned by anyone). v126: an automation's card (a
278
+ // delivery, a poll, a schedule, a form) files as automation, by the kind kept on the card, never by its name.
279
+ filedBy: { name: card.createdBy, kind: card.createdKind },
277
280
  deliverable: report ? "report" : "branch",
278
281
  planning: report ? "skip" : stage.planning ?? "auto",
279
282
  acceptance: grant === null ? ACCEPTANCE[kind] : grant.terms.acceptance[kind],
280
283
  ...(grant?.terms.perAttemptBudgetMicrousd == null ? {} : { budgetMicrousd: grant.terms.perAttemptBudgetMicrousd }),
281
284
  admittedRepos: [repo],
282
285
  }, now);
286
+ // v126: a card an outside event's delivery made gives its task a snapshot of where it came from, in this transaction.
287
+ const snapshot = made.ok ? snapshotOf(fresh.source) : null;
288
+ if (made.ok && snapshot !== null)
289
+ store.recordTaskSource(made.id, { flow: flow.id, card: fresh.id, entry: fresh.entry }, JSON.stringify(snapshot), now);
283
290
  const ranOnItsOwn = made.ok && grant !== null && sealFlowFilingUnderGrant(store, made.id, grant, fresh, now);
284
291
  if (made.ok)
285
292
  store.updateFlowCard(card.id, { task: made.id, ...(report || fresh.primaryTask !== null ? {} : { primaryTask: made.id }), waiting: ranOnItsOwn ? "Filed as a task, approved: this flow runs on its own" : "Filed as a task" }, now);
@@ -359,12 +366,24 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
359
366
  * 64 KiB), so in practice an attachment is never shortened. */
360
367
  const FLOW_ATTACHED_LIMIT = 128_000;
361
368
  export function flowGoalCuts(store, taskId, goal) {
369
+ let cuts = [];
362
370
  try {
363
- return goalCuts(store, taskId, goal);
371
+ cuts = goalCuts(store, taskId, goal);
364
372
  }
365
373
  catch {
366
- return [];
374
+ cuts = [];
367
375
  } // context, never a reason a build can't start
376
+ // v126: where the work came from, from the task's own kept source (or the task it revises) — never from the card's
377
+ // current task link or zone, which move on. Given whole whenever the goal itself couldn't carry it.
378
+ try {
379
+ const kept = store.taskSourceOf(taskId);
380
+ const snapshot = kept === null ? null : readSnapshot(kept.sourceJson);
381
+ const words = snapshot === null ? "" : referenceWords(snapshot);
382
+ if (words !== "" && !goal.includes(words) && !cuts.some(one => one.text === words))
383
+ cuts.push({ label: flowValueLabel("card.source"), text: words });
384
+ }
385
+ catch { /* context, never a reason a build can't start */ }
386
+ return cuts;
368
387
  }
369
388
  function goalCuts(store, taskId, goal) {
370
389
  const root = store.taskFamilyOf(taskId, null, true)?.root.id ?? taskId;
@@ -418,7 +437,7 @@ export function addCardToFlow(store, flow, input, actor, now) {
418
437
  if ("problem" in text)
419
438
  return { ok: false, message: text.problem };
420
439
  const stage = definition.stages.find(one => one.id === input.stage) ?? definition.stages.find(one => one.id === definition.start);
421
- const card = store.addFlowCard({ flow: flow.id, title: text.title, description: text.description, stage: stage.id, by: actor }, now);
440
+ const card = store.addFlowCard({ flow: flow.id, title: text.title, description: text.description, stage: stage.id, by: actor, byKind: "person" }, now);
422
441
  // Whoever adds a card follows it; owning it is a separate, deliberate step.
423
442
  store.setFlowCardWatcher(card, actor, true, now);
424
443
  return { ok: true, said: `Card added to ${zoneWords(stage.title)}`, card };
@@ -7,7 +7,7 @@
7
7
  import { brandMarkHtml } from "./brand-mark.js";
8
8
  import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
9
9
  import { choiceTargets } from "./flows.js";
10
- import { html, joinHtml, postForm } from "./html.js";
10
+ import { html, joinHtml, postForm, reauth } from "./html.js";
11
11
  export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
12
12
  `.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
13
13
  `.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
@@ -89,7 +89,7 @@ export function galleryUseHtml(input) {
89
89
  if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
90
90
  return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
91
91
  const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
92
- return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
92
+ return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
93
93
  };
94
94
  const toolsBlock = tools.length === 0 ? "" : html `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)}</section>`;
95
95
  // Made without a tool, the preview names the zone that needs it.
@@ -266,10 +266,26 @@ export const GALLERY = [
266
266
  does: ["Checks for outdated {{ask.outdated}} packages {{ask.schedule-words}}, with no AI.", "When some are out of date, updates them as a task, under your usual approvals.", "What you approve opens a pull request."],
267
267
  never: NEVER_MERGES,
268
268
  },
269
+ {
270
+ // v126: the direct road from a signed outside request to a build that waits for your approval.
271
+ id: "webhook-to-build", name: "Webhook to build", group: "health",
272
+ promise: "A signed request from any tool becomes a build, under your usual approvals.",
273
+ needs: ["A tool that can send signed webhooks", "GitHub"], asks: [],
274
+ steps: [
275
+ { id: "build", title: "Build it", kind: "task", instructions: work("{{card.title}}\n\n{{card.description}}") },
276
+ owner("review", "Review the build"),
277
+ { ...pr, next: "Done" },
278
+ { id: "done", title: "Done", kind: "done" },
279
+ ],
280
+ triggers: [{ kind: "webhook", sender: "standard", title: "Request" }],
281
+ does: ["Adds a webhook signed the Standard Webhooks way. Once the flow is made, open its Triggers and choose Set up under Signing: it shows the address and the secret together, once; give both to the sender.",
282
+ "Each signed request becomes a card and is built as a task. The task waits for your approval, like any other.", "You review the build; what you approve opens a pull request."],
283
+ never: NEVER_MERGES,
284
+ },
269
285
  {
270
286
  id: "error-to-fix", name: "Error to fix", group: "health", tools: ["sentry"],
271
- promise: "New errors from Sentry or any error tracker are sorted, researched and fixed.",
272
- needs: ["A webhook from Sentry or another tracker", SORTS], asks: [],
287
+ promise: "New Sentry errors are sorted, researched and fixed.",
288
+ needs: ["A Sentry Internal Integration", SORTS], asks: [],
273
289
  steps: [
274
290
  { id: "worth-fixing", title: "Worth fixing?", kind: "sort", question: "Is this error in our own code, and new or frequent enough to fix now?",
275
291
  answers: [
@@ -284,8 +300,9 @@ export const GALLERY = [
284
300
  { id: "done", title: "Done", kind: "done" },
285
301
  { id: "ignored", title: "Ignored", kind: "done" },
286
302
  ],
287
- triggers: [{ kind: "webhook", title: "Error", titleField: "data.issue.title", bodyField: "data.issue.culprit" }],
288
- does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Sentry or another error tracker.", "Jev keeps errors in your code that are new or frequent, and drops noise; ones it isn't sure about wait for you.",
303
+ // v126: signed by Sentry and read by Toolroll's Sentry reader: one card per issue, linked to it.
304
+ triggers: [{ kind: "webhook", sender: "sentry", title: "Error" }],
305
+ does: ["Adds a Sentry webhook. Once the flow is made, open its Triggers and choose New address; in Sentry create an Internal Integration (Settings → Developer Settings), paste that address as its Webhook URL, turn on Alert Rule Action and Issue events, then paste its Client Secret under Signing on the flow's Triggers.", "One card per Sentry issue, however often it alerts, linked to the issue.", "Jev keeps errors in your code that are new or frequent, and drops noise; ones it isn't sure about wait for you.",
289
306
  "Each kept error is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
290
307
  never: NEVER_MERGES,
291
308
  },
@@ -300,7 +317,7 @@ export const GALLERY = [
300
317
  pr,
301
318
  ],
302
319
  triggers: [{ kind: "webhook", title: "Incident", titleField: "data.attributes.name" }],
303
- does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
320
+ does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack. Better Stack doesn't sign its webhooks, so anyone with the address could add cards: keep it private.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
304
321
  never: NEVER_MERGES,
305
322
  },
306
323
  {
@@ -335,8 +352,9 @@ export const GALLERY = [
335
352
  pr,
336
353
  send("Send the fix"),
337
354
  ],
338
- triggers: [{ kind: "webhook", title: "Failed deployment", titleField: "payload.deployment.name" }],
339
- does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Vercel for failed deployments.", "Reads the build log and finds the cause.", "Builds a fix as a task, under your usual approvals, opens a pull request and sends it to you."],
355
+ // v126: signed by Vercel and read by Toolroll's Vercel reader: failed deployments only, one card each.
356
+ triggers: [{ kind: "webhook", sender: "vercel", title: "Failed deployment" }],
357
+ does: ["Adds a Vercel webhook. Once the flow is made, open its Triggers and choose New address; in Vercel (Settings → Webhooks) paste that address and choose Deployment Error, then paste the secret Vercel shows under Signing on the flow's Triggers. Put your Vercel project ID in Signing too, so only its deployments add cards.", "Reads the build log and finds the cause.", "Builds a fix as a task, under your usual approvals, opens a pull request and sends it to you."],
340
358
  never: NEVER_MERGES,
341
359
  },
342
360
  // ---------------------------------------------------------- Hear from users
@@ -637,7 +655,8 @@ export function useGalleryTemplate(store, template, repo, given, input) {
637
655
  const id = store.createFlow({ repo, name, definitionJson: JSON.stringify(built.definition), by: input.by }, input.now);
638
656
  const made = store.getFlow(id);
639
657
  for (const trigger of built.triggers) {
640
- const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir);
658
+ // No one is shown the gallery's addresses: a webhook is made with none until its panel makes one (v126).
659
+ const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false });
641
660
  if (!added.ok)
642
661
  throw new Error(added.message);
643
662
  if (added.reveal !== null)