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
@@ -0,0 +1,119 @@
1
+ /**
2
+ * How a webhook proves who sent it (docs/plans/webhook-tasks.md §1). Each sender's check runs over the raw bytes,
3
+ * before anything is parsed; a security header sent more than once, or not at all, is refused. Nothing here reads or
4
+ * writes the store: the caller passes the current generation's secret, and a refusal writes nothing.
5
+ */
6
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
7
+ import { WEBHOOK_SENDERS } from "./contracts/flow.js";
8
+ export { WEBHOOK_SENDERS };
9
+ export const SENDER_WORDS = {
10
+ none: "Unsigned", standard: "Standard Webhooks", "github-style": "GitHub-style signature", sentry: "Sentry", vercel: "Vercel",
11
+ };
12
+ /** A Standard Webhooks timestamp may be this far from now, either way. */
13
+ export const STANDARD_TOLERANCE_MS = 5 * 60_000;
14
+ const HEX = (bytes) => new RegExp(`^[0-9a-f]{${bytes * 2}}$`);
15
+ const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
16
+ const HIDDEN = /[\u0000-\u001f\u007f]/;
17
+ /** Exactly one value for a security header, or why not. */
18
+ function single(headers, name) {
19
+ const values = headers[name] ?? [];
20
+ if (values.length === 0)
21
+ return { ok: false, said: `The ${name} header is missing.` };
22
+ if (values.length > 1)
23
+ return { ok: false, said: `The ${name} header was sent more than once.` };
24
+ return { ok: true, value: values[0] };
25
+ }
26
+ function same(expected, given) {
27
+ return expected.length === given.length && timingSafeEqual(expected, given);
28
+ }
29
+ /** A `whsec_` secret's key bytes, or null when it isn't one. */
30
+ export function standardKey(secret) {
31
+ if (!secret.startsWith("whsec_"))
32
+ return null;
33
+ const encoded = secret.slice("whsec_".length);
34
+ if (!BASE64.test(encoded) || encoded.length % 4 !== 0)
35
+ return null;
36
+ const key = Buffer.from(encoded, "base64");
37
+ return key.length >= 24 && key.length <= 64 ? key : null;
38
+ }
39
+ /** Whether a delivery was signed with this secret by this sender. `none` proves nothing and is never passed here. */
40
+ export function proveDelivery(sender, secret, headers, body, now) {
41
+ switch (sender) {
42
+ case "github-style": {
43
+ const header = single(headers, "x-hub-signature-256");
44
+ if (!header.ok)
45
+ return header;
46
+ const given = header.value.trim().toLowerCase();
47
+ if (!given.startsWith("sha256=") || !HEX(32).test(given.slice(7)))
48
+ return { ok: false, said: "The signature isn't sha256=<hex>." };
49
+ return same(createHmac("sha256", secret).update(body).digest(), Buffer.from(given.slice(7), "hex")) ? { ok: true, signedId: null } : { ok: false, said: "Signature doesn't match." };
50
+ }
51
+ case "sentry": {
52
+ const header = single(headers, "sentry-hook-signature");
53
+ if (!header.ok)
54
+ return { ok: false, said: header.said.includes("missing") ? "Sentry didn't sign this: use an Internal Integration's webhook." : header.said };
55
+ const given = header.value.trim().toLowerCase();
56
+ if (!HEX(32).test(given))
57
+ return { ok: false, said: "The signature isn't a SHA-256 hex digest." };
58
+ return same(createHmac("sha256", secret).update(body).digest(), Buffer.from(given, "hex")) ? { ok: true, signedId: null } : { ok: false, said: "Signature doesn't match." };
59
+ }
60
+ case "vercel": {
61
+ const header = single(headers, "x-vercel-signature");
62
+ if (!header.ok)
63
+ return header;
64
+ const given = header.value.trim().toLowerCase();
65
+ if (!HEX(20).test(given))
66
+ return { ok: false, said: "The signature isn't a SHA-1 hex digest." };
67
+ return same(createHmac("sha1", secret).update(body).digest(), Buffer.from(given, "hex")) ? { ok: true, signedId: null } : { ok: false, said: "Signature doesn't match." };
68
+ }
69
+ case "standard": {
70
+ const key = standardKey(secret);
71
+ if (key === null)
72
+ return { ok: false, said: "The saved secret isn't a whsec_ secret." };
73
+ const id = single(headers, "webhook-id"), stamp = single(headers, "webhook-timestamp"), signature = single(headers, "webhook-signature");
74
+ for (const header of [id, stamp, signature])
75
+ if (!header.ok)
76
+ return header;
77
+ const idText = id.value, stampText = stamp.value, signatureText = signature.value;
78
+ if (idText === "" || idText.length > 200 || HIDDEN.test(idText))
79
+ return { ok: false, said: "The webhook-id header isn't usable." };
80
+ // Unix seconds, as an integer: milliseconds, decimals and signs are refused, not guessed at.
81
+ if (!/^\d{1,11}$/.test(stampText))
82
+ return { ok: false, said: "The webhook-timestamp isn't Unix seconds." };
83
+ if (Math.abs(now.getTime() - Number(stampText) * 1000) > STANDARD_TOLERANCE_MS)
84
+ return { ok: false, said: "Too old, or from the future." };
85
+ const expected = createHmac("sha256", key).update(Buffer.concat([Buffer.from(`${idText}.${stampText}.`, "utf8"), body])).digest();
86
+ // Any well-formed v1 entry may match; a malformed entry is passed over, and none well-formed is a refusal.
87
+ let wellFormed = 0;
88
+ for (const entry of signatureText.split(" ")) {
89
+ if (!entry.startsWith("v1,"))
90
+ continue;
91
+ const encoded = entry.slice(3);
92
+ if (!BASE64.test(encoded) || encoded.length % 4 !== 0)
93
+ continue;
94
+ const given = Buffer.from(encoded, "base64");
95
+ if (given.length !== 32)
96
+ continue;
97
+ wellFormed++;
98
+ if (same(expected, given))
99
+ return { ok: true, signedId: idText };
100
+ }
101
+ return { ok: false, said: wellFormed === 0 ? "The webhook-signature has no v1 signature." : "Signature doesn't match." };
102
+ }
103
+ }
104
+ }
105
+ /** Whether a pasted secret has its sender's shape (Sentry's and Vercel's come from them; the others can be made here). */
106
+ export function secretShapeOk(sender, secret) {
107
+ switch (sender) {
108
+ case "sentry": return /^[0-9a-fA-F]{32,128}$/.test(secret);
109
+ case "vercel": return /^[A-Za-z0-9]{16,200}$/.test(secret);
110
+ case "standard": return standardKey(secret) !== null;
111
+ case "github-style": return /^[\x21-\x7e]{16,200}$/.test(secret);
112
+ }
113
+ }
114
+ /** A new secret for a sender Toolroll can make one for; Sentry's and Vercel's are pasted from them. */
115
+ export function makeSecret(sender) {
116
+ return sender === "standard" ? `whsec_${randomBytes(32).toString("base64")}` : randomBytes(32).toString("hex");
117
+ }
118
+ /** Whether Toolroll makes this sender's secret (and shows it once), rather than asking for the sender's own. */
119
+ export const makesSecret = (sender) => sender === "standard" || sender === "github-style";
@@ -47,6 +47,8 @@ export type WorkIndexOptions = {
47
47
  state?: TaskState;
48
48
  leadId?: string;
49
49
  root?: string;
50
+ /** Only these families (by root task id): the sidebar reads its rows' own words this way. */
51
+ roots?: readonly string[];
50
52
  };
51
53
  /** How the list is grouped: the Needs you rows by their ask, then building, then the rest. */
52
54
  export type WorkIndexGroup = Ask | 'building' | 'rest';
@@ -85,6 +87,33 @@ export declare function workIndexPage(store: Store, now: Date, access: WorkSumma
85
87
  * permission-bound and bypasses pagination, never action-time verification. */
86
88
  export declare function workIndexTask(store: Store, taskId: string, now: Date, access: WorkSummaryAccess, root?: string): WorkIndexItem | null;
87
89
  export declare function workCountsByProject(store: Store, now: Date, access: WorkSummaryAccess): WorkProjectCounts[];
90
+ /** One row per task family the person may see (the Tasks list's own projection: current version, admission, unplaced
91
+ * work and the ceiling as the list applies them), with what the cockpit's states read (agent-state.ts): its code, whether
92
+ * it waits on a person, whether a lead took it on, and its live, unfinished and result runs. Every family, no page. */
93
+ export type WorkStateRow = {
94
+ repo: string | null;
95
+ rootId: string;
96
+ code: string;
97
+ needs: boolean;
98
+ personNeed: boolean;
99
+ leadClaim: boolean;
100
+ liveRun: number | null;
101
+ unfinished: number | null;
102
+ resultRun: number | null;
103
+ /** The Tasks list ranks it as waiting its turn (custody confirmations from another computer included). */
104
+ waits: boolean;
105
+ /** What it needs now, as an identity: its code and the question, hold, result or stop it is about. A new one of
106
+ * these — or a changed plan or approval terms (its digests) — is a new thing to need a person, even on the same task. */
107
+ version: string;
108
+ /** When what it needs now began: the newest of its plan's proposal, its question, its hold, its result and its stop
109
+ * (each fixed once written, so an unrelated edit never moves it). Newer asks are named first. */
110
+ at: string;
111
+ /** The agent its ask is about: the live one, or the one whose question or decision it parked on. */
112
+ askRun: number | null;
113
+ /** Every task_ref of the family (its versions), for counting each of its live attempts. */
114
+ members: number[];
115
+ };
116
+ export declare function workStateRows(store: Store, now: Date, access: WorkSummaryAccess): WorkStateRow[];
88
117
  /** Families whose headline is Ready for review, newest first: the Tasks
89
118
  * list's own rows, so a completed family (root or any revision), a failed
90
119
  * check and a superseded version never count. */
@@ -388,7 +388,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
388
388
  const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
389
389
  const filter = view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1' : view === 'completed' ? "code='complete'" : '1';
390
390
  const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
391
- SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($cursorRoot=0 OR
391
+ SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
392
392
  ${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
393
393
  ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
394
394
  ), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS} FROM ranked), project_totals AS (
@@ -415,7 +415,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
415
415
  'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
416
416
  'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
417
417
  'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
418
- .all({ ...parameters(now, access, options, custody), $taskId: taskId, $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
418
+ .all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
419
419
  const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
420
420
  const page = selected.slice(0, limit), last = page.at(-1);
421
421
  const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
@@ -442,6 +442,20 @@ export function workCountsByProject(store, now, access) {
442
442
  .all({ ...parameters(now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
443
443
  .map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
444
444
  }
445
+ export function workStateRows(store, now, access) {
446
+ const projection = registerValidators(store);
447
+ return store.handle.prepare(`${projection} SELECT repo,root_id,code,needs,person_need,lead_claim,live_run,unfinished,result_id,rank,
448
+ code||':'||COALESCE(question_id,'')||':'||COALESCE(agent_ask,'')||':'||COALESCE(hold_id,'')||':'||COALESCE(result_id,'')||':'||COALESCE(stop_run,'')||':'||COALESCE(digest,'')||':'||COALESCE(scope_digest,'') version,
449
+ COALESCE(NULLIF(MAX(COALESCE(proposed_at,''),COALESCE(finished_at,''),
450
+ COALESCE((SELECT created_at FROM decision WHERE id=question_id),''),COALESCE((SELECT asked_at FROM agent_question WHERE id=agent_ask),''),
451
+ COALESCE((SELECT held_at FROM hold WHERE id=hold_id),''),COALESCE((SELECT requested_at FROM run_stop WHERE run=stop_run),'')),''),sort_at) ask_at,
452
+ COALESCE(live_run,(SELECT run FROM decision WHERE id=question_id),(SELECT run FROM agent_question WHERE id=agent_ask)) ask_run,
453
+ (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members FROM ranked`)
454
+ .all(parameters(now, access, {}, custodyReadings(store)))
455
+ .map(row => ({ repo: s(row, 'repo'), rootId: String(row['root_id']), code: String(row['code']), needs: n(row, 'needs') === 1, personNeed: n(row, 'person_need') === 1,
456
+ leadClaim: n(row, 'lead_claim') === 1, liveRun: row['live_run'] == null ? null : n(row, 'live_run'), unfinished: row['unfinished'] == null ? null : n(row, 'unfinished'),
457
+ resultRun: row['result_id'] == null ? null : n(row, 'result_id'), waits: Number(row['rank']) === 2, version: String(row['version']), at: String(row['ask_at'] ?? ''), askRun: row['ask_run'] == null ? null : n(row, 'ask_run'), members: JSON.parse(String(row['members'] ?? '[]')).map(Number) }));
458
+ }
445
459
  /** Families whose headline is Ready for review, newest first: the Tasks
446
460
  * list's own rows, so a completed family (root or any revision), a failed
447
461
  * check and a superseded version never count. */
@@ -6,7 +6,9 @@ const MAX_AGE_MS = 60_000;
6
6
  const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_delivery', 'sqlite_sequence',
7
7
  'team_read', 'team_request', 'push_delivery', 'chat_message_ref',
8
8
  // Who is watching an agent right now: renewed by every open page's keep-alive, never news.
9
- 'agent_view']);
9
+ 'agent_view',
10
+ // A run's How it ran, derived from its own records on first view (run-efficiency.ts): a cache, never news.
11
+ 'run_efficiency']);
10
12
  const QUIET = {
11
13
  runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
12
14
  watch_episode: ['ticks'], provider_readiness: ['observed_at'],
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.63",
3
+ "version": "0.9.65",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsc -p tsconfig.build.json && node scripts/browser-build.mjs && node scripts/postbuild.mjs",
10
- "test": "vitest run",
10
+ "test": "vitest run --exclude '**/*.slow.test.ts'",
11
+ "test:slow": "vitest run .slow.test",
12
+ "test:all": "vitest run",
11
13
  "test:attacks": "vitest run --config test/attacks/vitest.config.ts",
12
14
  "check:quick": "node scripts/quick-check.mjs",
13
15
  "dev": "tsx src/cli.ts",
@@ -80,6 +82,9 @@
80
82
  "@types/node": "26.6.4",
81
83
  "@types/react": "19.3.0",
82
84
  "@types/react-dom": "19.3.0",
85
+ "@types/ws": "8.18.2",
86
+ "@xterm/addon-fit": "0.11.0",
87
+ "@xterm/xterm": "6.0.0",
83
88
  "@xyflow/react": "12.12.0",
84
89
  "class-variance-authority": "0.7.1",
85
90
  "clsx": "2.1.1",
@@ -124,7 +129,11 @@
124
129
  "mailparser": "3.9.36",
125
130
  "nodemailer": "10.0.15",
126
131
  "typescript-parser": "npm:typescript@6.0.3",
132
+ "ws": "8.22.0",
127
133
  "yaml": "2.9.1",
128
134
  "zod": "^4.6.5"
135
+ },
136
+ "optionalDependencies": {
137
+ "@lydell/node-pty": "1.2.0-beta.15"
129
138
  }
130
139
  }