toolroll 0.9.63 → 0.9.67

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 (302) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +33 -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 +117 -2
  16. package/dist/agent-fence.js +439 -12
  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 +39 -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/batch-checks.js +3 -2
  32. package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
  33. package/dist/browser/terminal.css +37 -0
  34. package/dist/browser/terminal.js +228 -0
  35. package/dist/browser/workspace.css +2 -2
  36. package/dist/browser/workspace.js +42 -38
  37. package/dist/browser-shell.js +6 -3
  38. package/dist/browser-workspace.d.ts +235 -1
  39. package/dist/browser-workspace.js +60 -0
  40. package/dist/builder.d.ts +10 -1
  41. package/dist/builder.js +87 -24
  42. package/dist/chat-actions.d.ts +13 -1
  43. package/dist/chat-actions.js +197 -15
  44. package/dist/chat-ask.d.ts +4 -0
  45. package/dist/chat-ask.js +14 -0
  46. package/dist/chat-channel.js +20 -0
  47. package/dist/chat-continuity.js +5 -4
  48. package/dist/chat-decide.d.ts +0 -14
  49. package/dist/chat-decide.js +4 -1
  50. package/dist/chat-delivery.d.ts +9 -0
  51. package/dist/chat-delivery.js +45 -6
  52. package/dist/chat-messages.d.ts +8 -0
  53. package/dist/chat-messages.js +36 -4
  54. package/dist/check-levels-ui.js +2 -2
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +19 -0
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +4 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +21 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/console-api.d.ts +13 -4
  71. package/dist/contracts/console-api.js +13 -4
  72. package/dist/contracts/flow-card.d.ts +4 -0
  73. package/dist/contracts/flow-card.js +3 -0
  74. package/dist/contracts/flow.d.ts +46 -0
  75. package/dist/contracts/flow.js +11 -2
  76. package/dist/contracts/lead-tools.d.ts +92 -0
  77. package/dist/contracts/lead-tools.js +13 -0
  78. package/dist/contracts/provider.d.ts +11 -1
  79. package/dist/contracts/provider.js +1 -1
  80. package/dist/contracts/route.d.ts +76 -1
  81. package/dist/contracts/route.js +1 -1
  82. package/dist/contracts/store-json.d.ts +2 -0
  83. package/dist/control-setup.d.ts +1 -1
  84. package/dist/control-ui.js +2 -2
  85. package/dist/converse.d.ts +16 -0
  86. package/dist/converse.js +32 -4
  87. package/dist/demo-chat.js +4 -4
  88. package/dist/demo.js +2 -2
  89. package/dist/desktop-update-gate.js +6 -0
  90. package/dist/desktop-update.d.ts +1 -1
  91. package/dist/discord-chat.js +4 -1
  92. package/dist/doctor.js +1 -1
  93. package/dist/egress-bridge.d.ts +17 -0
  94. package/dist/egress-bridge.js +111 -0
  95. package/dist/egress.d.ts +119 -0
  96. package/dist/egress.js +423 -0
  97. package/dist/exec.d.ts +78 -0
  98. package/dist/exec.js +367 -9
  99. package/dist/export-ui.js +2 -2
  100. package/dist/flow-engine.js +24 -5
  101. package/dist/flow-gallery-ui.js +2 -2
  102. package/dist/flow-gallery.js +27 -8
  103. package/dist/flow-share.js +16 -5
  104. package/dist/flow-triggers.d.ts +77 -8
  105. package/dist/flow-triggers.js +517 -70
  106. package/dist/flows-cli.js +8 -2
  107. package/dist/flows-ui.d.ts +0 -2
  108. package/dist/flows-ui.js +52 -4
  109. package/dist/flows.d.ts +4 -0
  110. package/dist/flows.js +9 -1
  111. package/dist/html.d.ts +9 -0
  112. package/dist/html.js +11 -0
  113. package/dist/invoke.d.ts +35 -0
  114. package/dist/invoke.js +347 -28
  115. package/dist/keys.d.ts +18 -14
  116. package/dist/keys.js +5 -0
  117. package/dist/lead-capabilities.js +3 -2
  118. package/dist/lead-contract.js +2 -0
  119. package/dist/lead-doors.d.ts +2 -0
  120. package/dist/lead-doors.js +7 -0
  121. package/dist/lead-follow.d.ts +2 -0
  122. package/dist/lead-follow.js +58 -2
  123. package/dist/lead-progress.js +1 -0
  124. package/dist/lead-queue-runtime.d.ts +46 -0
  125. package/dist/lead-queue-runtime.js +191 -0
  126. package/dist/lead-queue.d.ts +104 -0
  127. package/dist/lead-queue.js +187 -0
  128. package/dist/lead-sweep.d.ts +67 -0
  129. package/dist/lead-sweep.js +99 -0
  130. package/dist/lead-tools.d.ts +22 -0
  131. package/dist/lead-tools.js +152 -1
  132. package/dist/lead.d.ts +13 -5
  133. package/dist/lead.js +89 -29
  134. package/dist/limits-ui.d.ts +8 -1
  135. package/dist/limits-ui.js +26 -0
  136. package/dist/live-bus.d.ts +8 -0
  137. package/dist/live-bus.js +40 -4
  138. package/dist/live-detail.d.ts +27 -0
  139. package/dist/live-detail.js +57 -0
  140. package/dist/live-notes.d.ts +20 -0
  141. package/dist/live-notes.js +47 -0
  142. package/dist/live-records.d.ts +140 -0
  143. package/dist/live-records.js +935 -0
  144. package/dist/live-terminal.d.ts +269 -0
  145. package/dist/live-terminal.js +686 -0
  146. package/dist/live.d.ts +38 -27
  147. package/dist/live.js +142 -124
  148. package/dist/mcp-oauth.d.ts +6 -3
  149. package/dist/mcp-oauth.js +8 -4
  150. package/dist/memory-pass.js +2 -2
  151. package/dist/mobile-viewport.d.ts +5 -1
  152. package/dist/mobile-viewport.js +18 -2
  153. package/dist/model-catalog.js +4 -0
  154. package/dist/monitoring-ui.js +2 -2
  155. package/dist/needs-you.d.ts +1 -1
  156. package/dist/needs-you.js +11 -4
  157. package/dist/operate.d.ts +4 -0
  158. package/dist/operate.js +335 -19
  159. package/dist/passwordless.d.ts +23 -0
  160. package/dist/passwordless.js +26 -0
  161. package/dist/peek-cli.d.ts +12 -4
  162. package/dist/peek-cli.js +19 -11
  163. package/dist/phase-routing.js +10 -3
  164. package/dist/planner-source.d.ts +3 -0
  165. package/dist/planner-source.js +14 -0
  166. package/dist/planner.d.ts +2 -1
  167. package/dist/planner.js +7 -1
  168. package/dist/policy-ui.js +2 -2
  169. package/dist/policy.d.ts +1 -20
  170. package/dist/policy.js +29 -2
  171. package/dist/project-delete-ui.d.ts +18 -0
  172. package/dist/project-delete-ui.js +60 -9
  173. package/dist/project-delete.js +2 -2
  174. package/dist/project-memory.d.ts +3 -1
  175. package/dist/project-memory.js +57 -25
  176. package/dist/project-tools.d.ts +5 -3
  177. package/dist/project-tools.js +12 -9
  178. package/dist/provider-auth.d.ts +4 -3
  179. package/dist/provider-auth.js +5 -4
  180. package/dist/provider-connection.d.ts +2 -1
  181. package/dist/provider-connection.js +5 -0
  182. package/dist/provider.d.ts +25 -2
  183. package/dist/provider.js +63 -9
  184. package/dist/pull-request-ui.js +2 -2
  185. package/dist/push.js +1 -1
  186. package/dist/quick-start.d.ts +24 -0
  187. package/dist/quick-start.js +127 -0
  188. package/dist/release-adapters.d.ts +6 -0
  189. package/dist/release-adapters.js +70 -3
  190. package/dist/release.d.ts +10 -1
  191. package/dist/release.js +34 -15
  192. package/dist/request-budget-ui.js +2 -2
  193. package/dist/result-follow-ups.js +3 -2
  194. package/dist/result-review.d.ts +1 -1
  195. package/dist/result-review.js +9 -3
  196. package/dist/retention-ui.js +2 -2
  197. package/dist/retention.js +10 -1
  198. package/dist/run-efficiency.d.ts +73 -0
  199. package/dist/run-efficiency.js +228 -0
  200. package/dist/runner.d.ts +5 -1
  201. package/dist/runner.js +22 -1
  202. package/dist/scope.d.ts +183 -5
  203. package/dist/scope.js +256 -31
  204. package/dist/scout-net.d.ts +4 -1
  205. package/dist/scout-net.js +37 -12
  206. package/dist/scout.d.ts +20 -6
  207. package/dist/scout.js +27 -3
  208. package/dist/serve.js +248 -40
  209. package/dist/server/chat.d.ts +1 -1
  210. package/dist/server/chat.js +122 -31
  211. package/dist/server/chrome.d.ts +16 -2
  212. package/dist/server/chrome.js +63 -11
  213. package/dist/server/console.css +107 -42
  214. package/dist/server/flows.js +38 -8
  215. package/dist/server/guards.d.ts +1 -0
  216. package/dist/server/guards.js +23 -9
  217. package/dist/server/http.d.ts +7 -0
  218. package/dist/server/http.js +3 -2
  219. package/dist/server/live.d.ts +34 -1
  220. package/dist/server/live.js +39 -17
  221. package/dist/server/pages-assets.js +5 -0
  222. package/dist/server/pages.js +90 -13
  223. package/dist/server/people-tokens.d.ts +3 -0
  224. package/dist/server/people-tokens.js +55 -17
  225. package/dist/server/remote-hooks.js +12 -6
  226. package/dist/server/remote.d.ts +2 -1
  227. package/dist/server/remote.js +14 -5
  228. package/dist/server/render-chat.d.ts +12 -3
  229. package/dist/server/render-chat.js +47 -16
  230. package/dist/server/render-pages.d.ts +32 -4
  231. package/dist/server/render-pages.js +83 -13
  232. package/dist/server/render-people.d.ts +3 -2
  233. package/dist/server/render-people.js +3 -2
  234. package/dist/server/render-results.d.ts +18 -2
  235. package/dist/server/render-results.js +37 -28
  236. package/dist/server/render-settings.js +6 -5
  237. package/dist/server/render-tasks.d.ts +35 -10
  238. package/dist/server/render-tasks.js +111 -27
  239. package/dist/server/request-context.d.ts +2 -0
  240. package/dist/server/request-context.js +2 -1
  241. package/dist/server/route-table.d.ts +3 -0
  242. package/dist/server/route-table.js +47 -5
  243. package/dist/server/runtime.d.ts +33 -2
  244. package/dist/server/session.d.ts +25 -1
  245. package/dist/server/session.js +18 -4
  246. package/dist/server/settings.js +142 -10
  247. package/dist/server/sweeps.d.ts +4 -0
  248. package/dist/server/sweeps.js +58 -0
  249. package/dist/server/task-views.d.ts +2 -1
  250. package/dist/server/task-views.js +96 -6
  251. package/dist/server/tasks.js +327 -78
  252. package/dist/server/terminals.d.ts +31 -0
  253. package/dist/server/terminals.js +365 -0
  254. package/dist/setup-guide.js +3 -2
  255. package/dist/slack-chat.js +4 -1
  256. package/dist/spend-ui.js +3 -3
  257. package/dist/spend.js +2 -1
  258. package/dist/steer-capabilities.d.ts +32 -0
  259. package/dist/steer-capabilities.js +60 -0
  260. package/dist/steer-controls.d.ts +14 -0
  261. package/dist/steer-controls.js +36 -0
  262. package/dist/steer-status.js +2 -2
  263. package/dist/storage-ui.js +4 -4
  264. package/dist/store-baseline.d.ts +1 -1
  265. package/dist/store-baseline.js +1 -1
  266. package/dist/store.d.ts +440 -15
  267. package/dist/store.js +1072 -52
  268. package/dist/subagent-desk.js +1 -1
  269. package/dist/summary.js +3 -0
  270. package/dist/task-source.d.ts +58 -0
  271. package/dist/task-source.js +93 -0
  272. package/dist/team-leads.js +1 -1
  273. package/dist/teams-chat.js +4 -1
  274. package/dist/teardown-git.d.ts +22 -0
  275. package/dist/teardown-git.js +136 -2
  276. package/dist/telegram-lead.d.ts +3 -0
  277. package/dist/telegram-lead.js +44 -7
  278. package/dist/telegram.d.ts +11 -2
  279. package/dist/telegram.js +51 -17
  280. package/dist/terminal-catalog.d.ts +11 -0
  281. package/dist/terminal-catalog.js +129 -0
  282. package/dist/text-limits.d.ts +2 -0
  283. package/dist/text-limits.js +2 -0
  284. package/dist/tool-broker.d.ts +14 -0
  285. package/dist/tool-broker.js +67 -2
  286. package/dist/toolroll-update-ui.js +2 -2
  287. package/dist/toolroll-update.js +13 -3
  288. package/dist/tools-ui.js +2 -2
  289. package/dist/unfenced-test-support.d.ts +2 -0
  290. package/dist/unfenced-test-support.js +11 -0
  291. package/dist/webhook-limits.d.ts +23 -0
  292. package/dist/webhook-limits.js +30 -0
  293. package/dist/webhook-readers.d.ts +56 -0
  294. package/dist/webhook-readers.js +138 -0
  295. package/dist/webhook-signing.d.ts +27 -0
  296. package/dist/webhook-signing.js +119 -0
  297. package/dist/work-index.d.ts +29 -0
  298. package/dist/work-index.js +16 -2
  299. package/dist/workspace-revision.js +3 -1
  300. package/dist/writes-mode.d.ts +9 -0
  301. package/dist/writes-mode.js +8 -0
  302. package/package.json +15 -2
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The personal lead queue (docs/plans/lead-message-queue.md): a message sent while one of its author's personal lead
3
+ * turns runs (or while earlier ones wait) waits here and runs one turn per message, in order — the same machinery
4
+ * shared team conversations use (team-leads.ts), for the person's own threads (/chat, a task's or a project's chat).
5
+ *
6
+ * The row's words are its lead_message (pending until the engine's admission binds it, which clears pending in the
7
+ * same statement). Admission itself is the engine's (runLeadTurn with queuedMessageId + onAdmitted): this module only
8
+ * decides queue-or-run at send, claims the backlog's oldest row, binds, finishes, defers, removes and expires.
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ /** At most this many messages wait per person, and this many characters in all; a waiting message expires after 7 days. */
12
+ export const LEAD_QUEUE_MAX_MESSAGES = 10;
13
+ export const LEAD_QUEUE_MAX_CHARS = 20_000;
14
+ export const LEAD_QUEUE_EXPIRY_MS = 7 * 86_400_000;
15
+ /** The identity of a send: the same request with the same words and context is the same message. */
16
+ export function leadQueuePayloadHash(thread, text, context) {
17
+ return createHash("sha256").update(JSON.stringify([thread, text, context === null ? null : canonical(context)])).digest("hex");
18
+ }
19
+ const canonical = (context) => Object.fromEntries(Object.entries(context).filter(([, value]) => value !== undefined).sort(([a], [b]) => a.localeCompare(b)));
20
+ const readRow = (row) => ({
21
+ message: Number(row["message"]), thread: Number(row["thread"]), approver: String(row["approver"]), approverGeneration: Number(row["approver_generation"]),
22
+ session: Number(row["session"]), ceilingDigest: String(row["ceiling_digest"]), requestId: String(row["request_id"]), payloadHash: String(row["payload_hash"]),
23
+ context: row["context_json"] === null ? null : JSON.parse(String(row["context_json"])), status: String(row["status"]),
24
+ revision: Number(row["revision"]), generation: Number(row["generation"]), runner: row["runner"] === null ? null : String(row["runner"]),
25
+ turnId: row["turn_id"] === null ? null : Number(row["turn_id"]), error: row["error"] === null ? null : String(row["error"]),
26
+ createdAt: String(row["created_at"]), text: String(row["text"] ?? ""),
27
+ });
28
+ const SELECT = "SELECT q.*, m.text FROM lead_queue q JOIN lead_message m ON m.id = q.message";
29
+ export class PersonalQueue {
30
+ store;
31
+ constructor(store) {
32
+ this.store = store;
33
+ }
34
+ get db() { return this.store.handle; }
35
+ /** The approver's backlog: rows waiting or claimed but not yet bound to a turn, oldest first. */
36
+ backlog(approver) {
37
+ return this.db.prepare(`${SELECT} WHERE q.approver = ? AND (q.status = 'queued' OR (q.status = 'running' AND q.turn_id IS NULL)) ORDER BY q.message`).all(approver).map(readRow);
38
+ }
39
+ /** Every queue row of a thread (what the thread shows next to its messages). */
40
+ forThread(thread) {
41
+ return this.db.prepare(`${SELECT} WHERE q.thread = ? ORDER BY q.message`).all(thread).map(readRow);
42
+ }
43
+ get(message) {
44
+ const row = this.db.prepare(`${SELECT} WHERE q.message = ?`).get(message);
45
+ return row === undefined ? null : readRow(row);
46
+ }
47
+ /**
48
+ * Send: run now, or wait — in ONE transaction, so a turn ending between the check and the insert can't strand a
49
+ * message. A request already saved with the same words and context is that message (never a second send, never a
50
+ * removed one resurrected); with different ones it conflicts. Busy means a personal lead turn of this person is
51
+ * queued or running, or something of theirs already waits.
52
+ */
53
+ sendOrQueue(input, now) {
54
+ return this.store.transact(() => {
55
+ const hash = leadQueuePayloadHash(input.thread, input.text, input.context);
56
+ const seen = this.db.prepare(`${SELECT} WHERE q.session = ? AND q.request_id = ?`).get(input.session, input.requestId);
57
+ if (seen !== undefined)
58
+ return String(seen["payload_hash"]) === hash ? { kind: "queued", row: readRow(seen), already: true } : { kind: "conflict" };
59
+ const live = this.db.prepare(`SELECT 1 FROM mate_turn WHERE approver = ? AND state IN ('queued','running')
60
+ AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread = mate_turn.thread) LIMIT 1`).get(input.approver);
61
+ const waiting = this.backlog(input.approver);
62
+ if (live === undefined && waiting.length === 0)
63
+ return { kind: "run" };
64
+ if (waiting.length >= LEAD_QUEUE_MAX_MESSAGES)
65
+ return { kind: "full", message: `${LEAD_QUEUE_MAX_MESSAGES} messages are already waiting. Remove one, or wait for a reply.` };
66
+ if (waiting.reduce((sum, one) => sum + one.text.length, 0) + input.text.length > LEAD_QUEUE_MAX_CHARS) {
67
+ return { kind: "full", message: "The messages waiting are already long. Remove one, or wait for a reply." };
68
+ }
69
+ const message = this.store.appendLeadMessage({ thread: input.thread, turn: null, role: "operator", text: input.text, pending: true }, now);
70
+ this.db.prepare(`INSERT INTO lead_queue (message, thread, approver, approver_generation, session, ceiling_digest, request_id, payload_hash, context_json, status, created_at)
71
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?)`).run(message, input.thread, input.approver, input.approverGeneration, input.session, input.ceilingDigest, input.requestId, hash, input.context === null ? null : JSON.stringify(canonical(input.context)), now.toISOString());
72
+ return { kind: "queued", row: this.get(message), already: false };
73
+ });
74
+ }
75
+ /**
76
+ * The next message to run: per person, only the oldest of their backlog, only while none of theirs runs, and only when
77
+ * `eligible` agrees (the runtime's admission stamp). Claiming keeps it in the backlog until its turn binds it.
78
+ */
79
+ claimNext(runner, now, eligible = () => true) {
80
+ return this.store.transact(() => {
81
+ // An uncertain row does not hold the person's later messages (unlike a team room): its outcome is shown on it, and
82
+ // an unknown cost already pauses chat through its own latch.
83
+ const heads = this.db.prepare(`${SELECT} WHERE q.status = 'queued'
84
+ AND NOT EXISTS (SELECT 1 FROM lead_queue r WHERE r.approver = q.approver AND r.status = 'running')
85
+ AND NOT EXISTS (SELECT 1 FROM lead_queue e WHERE e.approver = q.approver AND e.status = 'queued' AND e.message < q.message)
86
+ ORDER BY q.message LIMIT 50`).all().map(readRow);
87
+ for (const row of heads) {
88
+ if (!eligible(row))
89
+ continue;
90
+ const changed = this.db.prepare("UPDATE lead_queue SET status = 'running', generation = generation + 1, runner = ?, claimed_at = ? WHERE message = ? AND status = 'queued'")
91
+ .run(runner, now.toISOString(), row.message);
92
+ if (Number(changed.changes) !== 1)
93
+ continue;
94
+ return { ...row, status: "running", runner, generation: row.generation + 1 };
95
+ }
96
+ return null;
97
+ });
98
+ }
99
+ /** The claim still stands: running, this runner, this generation and revision, unbound or bound to `turn`. */
100
+ current(claim) {
101
+ return this.db.prepare("SELECT 1 FROM lead_queue WHERE message = ? AND status = 'running' AND runner = ? AND generation = ? AND revision = ?")
102
+ .get(claim.message, claim.runner, claim.generation, claim.revision) !== undefined;
103
+ }
104
+ /** Inside the engine's admission transaction (onAdmitted): the claim binds to the turn that took it, exactly once. */
105
+ bindTurn(claim, turnId) {
106
+ if (!this.current(claim))
107
+ return false;
108
+ const turn = this.store.getLeadTurn(turnId);
109
+ if (turn === null || turn.thread !== claim.thread || turn.approver !== claim.approver)
110
+ return false;
111
+ return Number(this.db.prepare("UPDATE lead_queue SET turn_id = ? WHERE message = ? AND turn_id IS NULL AND status = 'running' AND generation = ?")
112
+ .run(turnId, claim.message, claim.generation).changes) === 1;
113
+ }
114
+ /** The claim ends: answered (its turn's reply is in the thread), failed or cancelled (words kept as the error), or
115
+ * uncertain (an outcome that can't be confirmed — never sent again). A message that never got a turn loses its words. */
116
+ finish(claim, result, now) {
117
+ return this.store.transact(() => {
118
+ const row = this.db.prepare("SELECT turn_id FROM lead_queue WHERE message = ? AND status = 'running' AND runner = ? AND generation = ?").get(claim.message, claim.runner, claim.generation);
119
+ if (row === undefined)
120
+ return false;
121
+ if (result.turnId !== undefined && row["turn_id"] !== null && Number(row["turn_id"]) !== result.turnId)
122
+ return false;
123
+ this.db.prepare("UPDATE lead_queue SET status = ?, error = ?, turn_id = COALESCE(turn_id, ?), revision = revision + 1, settled_at = ? WHERE message = ?")
124
+ .run(result.status, result.error ?? null, result.turnId ?? null, now.toISOString(), claim.message);
125
+ if (row["turn_id"] === null && result.turnId === undefined)
126
+ this.forgetWords(claim.message);
127
+ return true;
128
+ });
129
+ }
130
+ /** A refusal before admission (a cap, a budget, a latch, another turn): back to waiting with the words to show. */
131
+ defer(claim, error) {
132
+ return Number(this.db.prepare(`UPDATE lead_queue SET status = 'queued', generation = generation + 1, revision = revision + 1, runner = NULL, claimed_at = NULL, error = ?
133
+ WHERE message = ? AND status = 'running' AND turn_id IS NULL AND runner = ? AND generation = ?`).run(error, claim.message, claim.runner, claim.generation).changes) === 1;
134
+ }
135
+ /** Remove a waiting message (its author only, while it still waits): its words are forgotten, its request kept. */
136
+ remove(approver, message, now) {
137
+ return this.store.transact(() => {
138
+ const row = this.db.prepare("SELECT status FROM lead_queue WHERE message = ? AND approver = ?").get(message, approver);
139
+ if (row === undefined)
140
+ return "not-found";
141
+ if (row["status"] !== "queued")
142
+ return "started";
143
+ this.db.prepare("UPDATE lead_queue SET status = 'cancelled', error = 'Removed.', revision = revision + 1, settled_at = ? WHERE message = ?").run(now.toISOString(), message);
144
+ this.forgetWords(message);
145
+ return "removed";
146
+ });
147
+ }
148
+ /** Settle rows whose bound turn ended (by its recorded state), expire week-old waiting rows, and mark a claim that
149
+ * never bound by its turn deadline uncertain (the binding is the proof; elapsed time never resends). */
150
+ reconcile(now, turnDeadlineMs) {
151
+ return this.store.transact(() => {
152
+ let changed = 0;
153
+ for (const row of this.db.prepare(`SELECT q.message, t.state, t.failure_reason FROM lead_queue q JOIN mate_turn t ON t.id = q.turn_id
154
+ WHERE q.status = 'running' AND t.state IN ('answered','failed')`).all()) {
155
+ const uncertain = this.store.leadTurnCostUnconfirmed(Number(this.db.prepare("SELECT turn_id FROM lead_queue WHERE message = ?").get(Number(row["message"]))["turn_id"]));
156
+ const status = row["state"] === "answered" ? "answered" : uncertain ? "uncertain" : "failed";
157
+ changed += Number(this.db.prepare("UPDATE lead_queue SET status = ?, error = ?, revision = revision + 1, settled_at = ? WHERE message = ? AND status = 'running'")
158
+ .run(status, status === "answered" ? null : String(row["failure_reason"] ?? "failed"), now.toISOString(), Number(row["message"])).changes);
159
+ }
160
+ const stale = new Date(now.getTime() - turnDeadlineMs).toISOString();
161
+ changed += Number(this.db.prepare("UPDATE lead_queue SET status = 'uncertain', error = 'Delivery could not be confirmed. Nothing was sent again.', revision = revision + 1, settled_at = ? WHERE status = 'running' AND turn_id IS NULL AND claimed_at < ?")
162
+ .run(now.toISOString(), stale).changes);
163
+ const old = new Date(now.getTime() - LEAD_QUEUE_EXPIRY_MS).toISOString();
164
+ for (const row of this.db.prepare("SELECT message FROM lead_queue WHERE status = 'queued' AND created_at < ?").all(old)) {
165
+ this.db.prepare("UPDATE lead_queue SET status = 'failed', error = 'Not sent: it waited too long.', revision = revision + 1, settled_at = ? WHERE message = ?").run(now.toISOString(), Number(row["message"]));
166
+ this.forgetWords(Number(row["message"]));
167
+ changed++;
168
+ }
169
+ return changed;
170
+ });
171
+ }
172
+ /** End every waiting message sent under a session (it ended, or its person's chat was revoked). */
173
+ cancelForSession(session, why, now) {
174
+ return this.store.transact(() => {
175
+ const rows = this.db.prepare("SELECT message FROM lead_queue WHERE session = ? AND status = 'queued'").all(session);
176
+ for (const row of rows) {
177
+ this.db.prepare("UPDATE lead_queue SET status = 'cancelled', error = ?, revision = revision + 1, settled_at = ? WHERE message = ?").run(why, now.toISOString(), Number(row["message"]));
178
+ this.forgetWords(Number(row["message"]));
179
+ }
180
+ return rows.length;
181
+ });
182
+ }
183
+ /** A message that never ran keeps no words, context or search copy (it stays pending, so never history). */
184
+ forgetWords(message) {
185
+ this.store.forgetQueuedLeadMessage(message);
186
+ }
187
+ }
@@ -0,0 +1,67 @@
1
+ import { type AssignmentSnapshot } from "./assignment.js";
2
+ import { type Store } from "./store.js";
3
+ import { type StopRequest } from "./task-control.js";
4
+ export declare const SWEEP_VERDICTS: readonly ["fixed", "not reproducible", "blocked", "done"];
5
+ export type SweepVerdict = typeof SWEEP_VERDICTS[number];
6
+ /** The line every sweep child's brief asks for, appended to the shared rules. */
7
+ export declare const VERDICT_RULE = "Start your handoff's conclusion with one line: `Verdict: fixed`, `Verdict: not reproducible`, `Verdict: blocked` or `Verdict: done`, then ` \u2014 ` and one sentence why (the root cause, what you tried, or what blocks it).";
8
+ /** A conclusion's verdict line, read strictly. */
9
+ export declare function verdictOf(conclusion: string | null | undefined): {
10
+ verdict: SweepVerdict;
11
+ why: string;
12
+ } | null;
13
+ export type SweepRow = {
14
+ ordinal: number;
15
+ label: string;
16
+ /** The child's family root: its identity in the sweep. */
17
+ task: string;
18
+ /** The task the family is on now (a revision of the child, after one): what an act on this row acts on. */
19
+ active: string;
20
+ state: AssignmentSnapshot["state"] | "missing";
21
+ detail: string;
22
+ verdict: {
23
+ verdict: SweepVerdict;
24
+ why: string;
25
+ } | null;
26
+ /** The child's result, when it has one. */
27
+ result: {
28
+ taskId: string;
29
+ runId: number;
30
+ } | null;
31
+ };
32
+ export type SweepTable = {
33
+ id: number;
34
+ title: string;
35
+ repo: string;
36
+ maxParallel: number;
37
+ rows: SweepRow[];
38
+ counts: {
39
+ settled: number;
40
+ working: number;
41
+ total: number;
42
+ byVerdict: Partial<Record<SweepVerdict, number>>;
43
+ };
44
+ };
45
+ /** A sweep as its table shows it; null when there is no such sweep or this reader may not see its project. */
46
+ export declare function sweepTable(store: Store, id: number, now: Date, repos: readonly string[], root?: string): SweepTable | null;
47
+ /** "8 of 11 settled — 5 fixed, 2 not reproducible, 1 blocked": the sweep in one line. */
48
+ export declare function sweepLine(table: SweepTable): string;
49
+ /** Not ready yet: working, being checked, or waiting on a decision — what "cancel the rest" acts on. */
50
+ export declare const SWEEP_OPEN: ReadonlySet<SweepRow["state"]>;
51
+ /** One open item exactly as it stands now: its active task, state, attempt (a running build to stop) and last change. */
52
+ export type SweepOpenItem = {
53
+ label: string;
54
+ task: string;
55
+ state: string;
56
+ run: number | null;
57
+ at: string | null;
58
+ scope: string | null;
59
+ };
60
+ /** Exactly what "cancel the rest" would act on now; any change makes a card or a form for it stale. */
61
+ export declare function sweepOpen(store: Store, table: SweepTable, now: Date): SweepOpenItem[];
62
+ export declare const sweepOpenDigest: (open: readonly SweepOpenItem[]) => string;
63
+ /**
64
+ * Cancel the rest: a running build is stopped through the ordinary stop (process-exit safeguards and all); anything
65
+ * else is cancelled. Every refusal is said, never swallowed. The caller has proved `open` is what stands now.
66
+ */
67
+ export declare function cancelSweepRest(store: Store, actor: string, open: readonly SweepOpenItem[], now: Date, via: StopRequest["via"]): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Lead fan-out (docs/plans/lead-fan-out.md): a sweep's table and verdicts, read from facts — never a model.
3
+ *
4
+ * Each row is a child's assignment (the same state words the board uses), its verdict, and its result. The verdict is
5
+ * the first line of the child's conclusion, `Verdict: fixed | not reproducible | blocked | done — why`, which the
6
+ * sweep's brief asks for. It is read strictly: anything else reads as no verdict, never a guess.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { withActor } from "./actor.js";
10
+ import { assignmentOf } from "./assignment.js";
11
+ import { chatTaskRun } from "./chat-task-actions.js";
12
+ import { verifiedAuthor } from "./store.js";
13
+ import { requestTaskStop } from "./task-control.js";
14
+ export const SWEEP_VERDICTS = ["fixed", "not reproducible", "blocked", "done"];
15
+ /** The line every sweep child's brief asks for, appended to the shared rules. */
16
+ export const VERDICT_RULE = "Start your handoff's conclusion with one line: `Verdict: fixed`, `Verdict: not reproducible`, `Verdict: blocked` or `Verdict: done`, then ` — ` and one sentence why (the root cause, what you tried, or what blocks it).";
17
+ /** A conclusion's verdict line, read strictly. */
18
+ export function verdictOf(conclusion) {
19
+ if (typeof conclusion !== "string")
20
+ return null;
21
+ const first = conclusion.split("\n", 1)[0].trim();
22
+ const match = /^Verdict:\s*(fixed|not reproducible|not-reproducible|blocked|done)\s*(?:—|–|-|:)\s*(.{1,300})$/i.exec(first);
23
+ if (match === null)
24
+ return null;
25
+ const verdict = match[1].toLowerCase().replace("-", " ");
26
+ return SWEEP_VERDICTS.includes(verdict) ? { verdict, why: match[2].trim() } : null;
27
+ }
28
+ const SETTLED = new Set(["ready-to-check", "complete", "cancelled", "needs-decision", "missing"]);
29
+ /** A sweep as its table shows it; null when there is no such sweep or this reader may not see its project. */
30
+ export function sweepTable(store, id, now, repos, root) {
31
+ const sweep = store.getSweep(id);
32
+ if (sweep === null || !repos.includes(sweep.repo))
33
+ return null;
34
+ const rows = store.sweepTasks(id).map(child => {
35
+ const assignment = assignmentOf(store, child.rootTask, now, { principal: "operator", repos }, root);
36
+ const run = assignment?.receipt?.runId ?? null;
37
+ const resultTask = assignment?.receipt?.taskId ?? null;
38
+ const conclusion = run === null ? null : store.getRun(run)?.handoff ?? null;
39
+ return {
40
+ ordinal: child.ordinal, label: child.label, task: child.rootTask, active: assignment?.activeTaskId ?? child.rootTask,
41
+ state: assignment?.state ?? "missing", detail: assignment?.detail ?? "This task is gone.",
42
+ verdict: verdictOf(conclusion),
43
+ result: run === null || resultTask === null ? null : { taskId: resultTask, runId: run },
44
+ };
45
+ });
46
+ const byVerdict = {};
47
+ for (const row of rows)
48
+ if (row.verdict !== null)
49
+ byVerdict[row.verdict.verdict] = (byVerdict[row.verdict.verdict] ?? 0) + 1;
50
+ const settled = rows.filter(row => SETTLED.has(row.state)).length;
51
+ return { id, title: sweep.title, repo: sweep.repo, maxParallel: sweep.maxParallel, rows,
52
+ counts: { settled, working: rows.length - settled, total: rows.length, byVerdict } };
53
+ }
54
+ /** "8 of 11 settled — 5 fixed, 2 not reproducible, 1 blocked": the sweep in one line. */
55
+ export function sweepLine(table) {
56
+ const verdicts = SWEEP_VERDICTS.filter(one => (table.counts.byVerdict[one] ?? 0) > 0).map(one => `${table.counts.byVerdict[one]} ${one}`);
57
+ return `${table.counts.settled} of ${table.counts.total} settled${verdicts.length === 0 ? "" : ` — ${verdicts.join(", ")}`}`;
58
+ }
59
+ /** Not ready yet: working, being checked, or waiting on a decision — what "cancel the rest" acts on. */
60
+ export const SWEEP_OPEN = new Set(["working", "checking", "needs-decision"]);
61
+ /** Exactly what "cancel the rest" would act on now; any change makes a card or a form for it stale. */
62
+ export function sweepOpen(store, table, now) {
63
+ return table.rows.filter(row => SWEEP_OPEN.has(row.state)).map(row => ({
64
+ label: row.label, task: row.active, state: row.state, run: chatTaskRun(store, row.active, "stop", now), at: store.getTask(row.active)?.updatedAt ?? null,
65
+ // The scope's identity too: a goal rewritten in between makes a card or form for it stale (Codex sweep r2).
66
+ scope: store.getScope(row.active)?.digest ?? null,
67
+ }));
68
+ }
69
+ export const sweepOpenDigest = (open) => createHash("sha256").update(JSON.stringify(open)).digest("hex");
70
+ /**
71
+ * Cancel the rest: a running build is stopped through the ordinary stop (process-exit safeguards and all); anything
72
+ * else is cancelled. Every refusal is said, never swallowed. The caller has proved `open` is what stands now.
73
+ */
74
+ export function cancelSweepRest(store, actor, open, now, via) {
75
+ let cancelled = 0, stopping = 0;
76
+ const refused = [];
77
+ for (const item of open) {
78
+ if (item.run !== null) {
79
+ const stop = requestTaskStop(store, { taskId: item.task, runId: item.run, by: verifiedAuthor(actor), via }, now);
80
+ if (stop.ok)
81
+ stopping += 1;
82
+ else
83
+ refused.push(`${item.label} (${stop.detail})`);
84
+ continue;
85
+ }
86
+ const result = withActor({ account: actor, lead: false }, () => store.cancelTask(item.task, now));
87
+ if (result.ok)
88
+ cancelled += 1;
89
+ else
90
+ refused.push(`${item.label} (${String(result.reason ?? "refused")})`);
91
+ }
92
+ const parts = [`Cancelled ${cancelled} item${cancelled === 1 ? "" : "s"}.`];
93
+ if (stopping > 0)
94
+ parts.push(`Stop requested for ${stopping} running build${stopping === 1 ? "" : "s"}: ${stopping === 1 ? "it stops" : "they stop"} at the next safe point; cancel ${stopping === 1 ? "it" : "them"} from the task page once stopped.`);
95
+ if (refused.length > 0)
96
+ parts.push(`Not cancelled: ${refused.join("; ")}.`);
97
+ parts.push("Finished results stay.");
98
+ return parts.join(" ");
99
+ }
@@ -48,6 +48,12 @@ export type LeadToolContext = {
48
48
  checkedCapabilities?: Map<string, number>;
49
49
  /** Records this turn's one question to its owner; false when it already asked one. Absent: this surface cannot ask. */
50
50
  ask?: (question: string, options: readonly string[]) => boolean;
51
+ /** This turn's tool discovery (find_tools): how many searches it made and which deferred tools it found, which the
52
+ * conversation loop advertises from its next request. Absent (a direct call outside a turn): nothing to add to. */
53
+ findTools?: {
54
+ searches: number;
55
+ found: Set<string>;
56
+ };
51
57
  };
52
58
  export type LeadToolResult = {
53
59
  ok: true;
@@ -105,6 +111,22 @@ export declare const ISO_STAMP_RULE: RegExp;
105
111
  * through a confirmed card into the one authenticated edit transaction.
106
112
  */
107
113
  export declare function agentsOver(store: Store, taskId: string, now: Date): Record<string, unknown> | null;
114
+ /**
115
+ * The tools every request advertises (docs/plans/lead-deferred-tools.md): the ones most replies use, and every tool a
116
+ * guard requires read in an earlier step (search_project_memory before any proposal, get_capabilities before work that
117
+ * needs an agent, get_result before propose_review, get_decision before propose_answer). The rest are found with
118
+ * find_tools and advertised from the next request of the same reply.
119
+ */
120
+ export declare const LEAD_CORE_TOOLS: ReadonlySet<string>;
121
+ /** At most this many find_tools searches per reply, results per search, and bytes of found tool schemas per reply. */
122
+ export declare const FIND_TOOLS_PER_TURN = 3;
123
+ export declare const FIND_TOOLS_MAX_RESULTS = 6;
124
+ export declare const FIND_TOOLS_SCHEMA_BYTES = 48000;
125
+ /** The deferred tools a query finds, best first: an exact name, then words in the name, keywords and description. */
126
+ export declare function findLeadTools(query: string): {
127
+ name: string;
128
+ summary: string;
129
+ }[];
108
130
  /** The lead's tools, in the order the model is shown them: each one's inputSchema is its contract, derived. */
109
131
  export declare const LEAD_TOOLS: LeadTool[];
110
132
  /** A unified diff, file by file, with each file's added and removed lines. */
@@ -1,3 +1,4 @@
1
+ import { sweepLine, sweepTable } from "./lead-sweep.js";
1
2
  import { TEXT_LIMITS } from "./text-limits.js";
2
3
  import { connectionsOf, oneClickOf } from "./mcp-connect.js";
3
4
  import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
@@ -372,7 +373,102 @@ export function agentsOver(store, taskId, now) {
372
373
  choices: Object.fromEntries(PHASES.filter(phase => phase !== "review").map(phase => [ROLE_WORD[phase], choices[phase].filter(one => one.selectable).map(one => ({ provider: one.provider, model: one.model, current: one.current }))])),
373
374
  };
374
375
  }
376
+ /**
377
+ * The tools every request advertises (docs/plans/lead-deferred-tools.md): the ones most replies use, and every tool a
378
+ * guard requires read in an earlier step (search_project_memory before any proposal, get_capabilities before work that
379
+ * needs an agent, get_result before propose_review, get_decision before propose_answer). The rest are found with
380
+ * find_tools and advertised from the next request of the same reply.
381
+ */
382
+ export const LEAD_CORE_TOOLS = new Set([
383
+ "find_tools", "get_brief", "recap", "list_repos", "get_project_context", "search_project_memory", "get_capabilities",
384
+ "list_tasks", "get_task", "get_result", "get_diff", "get_check_log", "list_decisions", "get_decision",
385
+ "get_actions", "get_action_status", "propose_action", "propose_task", "propose_scope", "propose_steer",
386
+ "propose_answer", "propose_review", "remember", "ask_owner", "commit_to",
387
+ // The lead's queue verbs: basic, and small enough that deferring them saves almost nothing.
388
+ "queue", "propose_next", "propose_reserve", "propose_hold", "propose_unhold", "propose_cancel",
389
+ // Task and result acts a reply uses often (links to a control, a result's images and evidence, the task's own acts):
390
+ // small, so deferral is kept for the large, occasional families (flows, subagents, sweeps, project tools).
391
+ "show_control", "get_controls", "get_result_images", "get_acceptance_evidence", "offer_approval", "propose_task_action", "propose_agents",
392
+ "get_agents", "propose_dependency_repair",
393
+ ]);
394
+ /** Words a deferred tool is found by, beyond its name and description. */
395
+ const LEAD_TOOL_KEYWORDS = {
396
+ propose_flow: ["flow", "automation", "workflow", "schedule", "trigger", "every", "when", "email", "inbox", "sort", "follow-up", "nudge"],
397
+ get_flows: ["flow", "automation", "workflow", "running", "card"],
398
+ get_flow_insights: ["flow", "insights", "stats", "how flows"],
399
+ propose_subagent: ["subagent", "assistant", "rep", "support", "sales", "teammate", "rules", "persona"],
400
+ get_subagents: ["subagent", "assistant", "teammate", "rules"],
401
+ propose_sweep: ["sweep", "fan-out", "many", "parallel", "batch", "each", "list of"],
402
+ get_sweep: ["sweep", "fan-out", "batch", "table", "verdict"],
403
+ propose_sweep_cancel: ["sweep", "cancel", "stop the rest"],
404
+ get_models: ["model", "models", "provider", "price"],
405
+ get_project_tools: ["tools", "mcp", "server", "integration", "connect", "stripe", "notion", "linear", "sentry", "jira"],
406
+ get_integrations: ["integration", "slack", "telegram", "discord", "teams", "email", "connected"],
407
+ get_skills: ["skill", "skills", "guide"],
408
+ get_project_knowledge: ["knowledge", "docs", "notes", "memory"],
409
+ get_task_conversation: ["conversation", "chat", "history", "messages"],
410
+ release_commitment: ["promise", "commitment", "release", "forget"],
411
+ get_person: ["person", "people", "who", "member", "teammate"],
412
+ };
413
+ /** At most this many find_tools searches per reply, results per search, and bytes of found tool schemas per reply. */
414
+ export const FIND_TOOLS_PER_TURN = 3;
415
+ export const FIND_TOOLS_MAX_RESULTS = 6;
416
+ export const FIND_TOOLS_SCHEMA_BYTES = 48_000;
417
+ const FIND_TOOLS_NONE = "Nothing matched. The other tools cover flows, subagents, sweeps, agents and models, project tools and integrations, controls and settings pages, holds and the queue, task actions (retry, stop, merge), result images and evidence, people, and promises — search again with one of those words.";
418
+ const MAKE_WORDS = new Set(["make", "add", "create", "set", "draft", "new", "start", "change", "edit", "build", "run", "hold", "cancel", "release", "move", "switch", "turn"]);
419
+ const LOOK_WORDS = new Set(["show", "list", "see", "what", "which", "how", "check", "read", "look", "find", "open", "where"]);
420
+ /** The deferred tools a query finds, best first: an exact name, then words in the name, keywords and description. */
421
+ export function findLeadTools(query) {
422
+ // A plural matches its singular ("sweeps" finds sweep tools).
423
+ const words = query.toLowerCase().split(/[^a-z0-9-]+/).filter(one => one.length >= 3).map(one => one.length > 4 && one.endsWith("s") && !one.endsWith("ss") ? one.slice(0, -1) : one);
424
+ const exact = query.trim().toLowerCase();
425
+ const scored = LEAD_TOOLS.filter(tool => !LEAD_CORE_TOOLS.has(tool.name)).map(tool => {
426
+ const name = tool.name.toLowerCase(), keywords = (LEAD_TOOL_KEYWORDS[tool.name] ?? []).join(" ").toLowerCase();
427
+ const description = tool.description.toLowerCase();
428
+ let score = name === exact ? 100 : 0;
429
+ for (const word of words)
430
+ score += (name.includes(word) ? 5 : 0) + (keywords.includes(word) ? 3 : 0) + (description.includes(word) ? 1 : 0);
431
+ // What the person wants done leans the match: making or changing something → a proposal; looking → a read.
432
+ if (score > 0 && words.some(word => MAKE_WORDS.has(word)))
433
+ score += name.startsWith("propose_") ? 4 : 0;
434
+ if (score > 0 && words.some(word => LOOK_WORDS.has(word)))
435
+ score += name.startsWith("get_") || name === "show_control" ? 4 : 0;
436
+ return { tool, score };
437
+ }).filter(one => one.score > 0).sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
438
+ return scored.slice(0, FIND_TOOLS_MAX_RESULTS).map(({ tool }) => ({ name: tool.name, summary: summaryLine(tool.description) }));
439
+ }
440
+ /** A description's first sentence, at most 160 characters. */
441
+ function summaryLine(description) {
442
+ const first = /^(.+?[.!?])(\s|$)/.exec(description)?.[1] ?? description;
443
+ return first.length <= 160 ? first : `${first.slice(0, 157).trimEnd()}…`;
444
+ }
375
445
  const LEAD_TOOL_HANDLERS = {
446
+ find_tools: {
447
+ // Counted before the call is read, so an invalid search still uses one of the reply's three.
448
+ prepare: (ctx, raw) => { if (ctx.findTools !== undefined)
449
+ ctx.findTools.searches++; return raw; },
450
+ description: "Find the lead's other tools by what you want to do — flows, subagents, sweeps, agents and models, project tools and integrations, controls and settings pages, holds and the queue, task actions (retry, stop, merge), result images and evidence, people, promises. Search before saying something can't be done. Matching tools become available from your next step in this reply: call them then, not in the same step.",
451
+ handle: (ctx, args) => {
452
+ const state = ctx.findTools;
453
+ if (state === undefined)
454
+ return { ok: false, message: "find_tools works inside a conversation reply." };
455
+ if (state.searches > FIND_TOOLS_PER_TURN)
456
+ return { ok: false, message: `You have searched ${FIND_TOOLS_PER_TURN} times this reply. Use the tools you found, or answer with what you have.` };
457
+ const bytesOf = (name) => Buffer.byteLength(JSON.stringify(LEAD_TOOL_SCHEMAS.find(one => one.name === name) ?? {}), "utf8");
458
+ let used = [...state.found].reduce((sum, name) => sum + bytesOf(name), 0);
459
+ const tools = [];
460
+ for (const one of findLeadTools(args.query)) {
461
+ if (!state.found.has(one.name)) {
462
+ if (used + bytesOf(one.name) > FIND_TOOLS_SCHEMA_BYTES)
463
+ continue;
464
+ used += bytesOf(one.name);
465
+ state.found.add(one.name);
466
+ }
467
+ tools.push(one);
468
+ }
469
+ return { ok: true, body: { tools, next: tools.length === 0 ? FIND_TOOLS_NONE : "These are available from your next step in this reply: call one then." } };
470
+ },
471
+ },
376
472
  get_brief: {
377
473
  description: 'Read current tasks, decisions, results and project knowledge from the local database. No model or mutation.',
378
474
  handle: (ctx, args) => {
@@ -1235,7 +1331,8 @@ const LEAD_TOOL_HANDLERS = {
1235
1331
  const thread = ctx.store.liveLeadThreadFor(ctx.who.name, { kind: "task", key: root });
1236
1332
  if (thread === null || thread.ceilingDigest !== ctx.who.ceilingDigest)
1237
1333
  return { ok: true, body: { task: root, messages: [], notice: "No conversation about this task yet." } };
1238
- const messages = ctx.store.listLeadMessages(thread.id, limit).map(one => ({ from: one.role === "operator" ? "operator" : "lead", text: one.text.length > 1_200 ? `${one.text.slice(0, 1_200)}…` : one.text, at: one.createdAt }));
1334
+ // What the lead reads of a task's conversation is what it has been told: admitted messages only (v128).
1335
+ const messages = ctx.store.listLeadHistory(thread.id, limit).map(one => ({ from: one.role === "operator" ? "operator" : "lead", text: one.text.length > 1_200 ? `${one.text.slice(0, 1_200)}…` : one.text, at: one.createdAt }));
1239
1336
  return { ok: true, body: { task: root, title: family?.root.title ?? null, messages, notice: messages.length === limit ? "Only the most recent messages are shown." : null } };
1240
1337
  },
1241
1338
  },
@@ -1640,6 +1737,60 @@ const LEAD_TOOL_HANDLERS = {
1640
1737
  return { ok: true, body: { proposal: id, kind: "task", repo: args["repo"], deliverable: report ? "report" : "branch", planning: report ? "not needed for a scout" : planning, awaiting: "the operator's confirmation" } };
1641
1738
  },
1642
1739
  },
1740
+ propose_sweep: {
1741
+ description: "Several independent items under ONE shared rule set (a bug sweep, a batch of small fixes): 2-20 items, each becomes its own task in its own worktree, started with one approval, at most max_parallel (default 3) at once, each reporting a verdict into one table. brief holds the shared rules every item gets (e.g. reproduce first; 'not reproducible' is a valid result; prove the fix); each item is only its own report. Not for one task, and not for steps that depend on each other.",
1742
+ handle: (ctx, args) => {
1743
+ const repo = repoPathOf(ctx.who, args["repo"]);
1744
+ if (repo === null)
1745
+ return { ok: false, message: "repo must be one of the ids from list_repos" };
1746
+ const items = Array.isArray(args["items"]) ? args["items"] : [];
1747
+ for (const one of [{ title: args["title"], goal: args["brief"] }, ...items.map(item => ({ title: item["title"], goal: item["item"] }))]) {
1748
+ if (typeof one.title !== "string" || typeof one.goal !== "string")
1749
+ return { ok: false, message: "each title, the brief and each item are text" };
1750
+ const bad = validateTaskText({ title: one.title, goal: one.goal, outOfScope: null });
1751
+ if (bad !== null)
1752
+ return { ok: false, message: bad.message };
1753
+ if ([one.title, one.goal].some(text => typeof text === "string" && scanForSecrets(text).length > 0))
1754
+ return { ok: false, message: "Task text cannot contain credentials." };
1755
+ }
1756
+ const acceptance = readAcceptanceArg(args["acceptance"]);
1757
+ if (acceptance === null)
1758
+ return { ok: false, message: "acceptance is required: at least one criterion with an id, statement, and evidence kinds (shared by every item)" };
1759
+ try {
1760
+ const action = prepareSharedAction(ctx.store, ctx.who, "sweep_file", { repo, title: args["title"], brief: args["brief"], items: items.map(item => ({ title: item["title"], item: item["item"] })),
1761
+ acceptance, maxParallel: typeof args["max_parallel"] === "number" ? args["max_parallel"] : 3 }, ctx.evidenceRoot, ctx.now);
1762
+ const id = ctx.draft("action", { ...action });
1763
+ return id === null ? tooMany() : { ok: true, body: { proposal: id, label: action.title, awaiting: "human review in the secure confirmation screen", executed: false } };
1764
+ }
1765
+ catch (error) {
1766
+ return { ok: false, message: error instanceof Error ? error.message : "That sweep couldn't be drafted." };
1767
+ }
1768
+ },
1769
+ },
1770
+ get_sweep: {
1771
+ description: "A sweep's table as facts: each item's state, its verdict (fixed, not reproducible, blocked, done, or none yet) and its result, plus the one-line count. Read this, don't poll each task.",
1772
+ handle: (ctx, args) => {
1773
+ const table = sweepTable(ctx.store, Number(args["sweep"]), ctx.now, ctx.who.repos, ctx.evidenceRoot);
1774
+ if (table === null)
1775
+ return { ok: false, message: "No such sweep in your projects." };
1776
+ return { ok: true, body: { sweep: table.id, title: table.title, line: sweepLine(table), table: `/sweeps/${table.id}`, maxParallel: table.maxParallel,
1777
+ items: table.rows.map(row => ({ item: row.ordinal + 1, label: row.label, task: row.task, state: row.state, detail: row.detail.slice(0, 300),
1778
+ verdict: row.verdict?.verdict ?? null, why: row.verdict?.why ?? null, result: row.result })) } };
1779
+ },
1780
+ },
1781
+ propose_sweep_cancel: {
1782
+ description: "Cancel every item of a sweep that isn't ready yet (finished results stay); one confirmation. To steer, cancel or retry ONE item, use that task's own actions.",
1783
+ handle: (ctx, args) => {
1784
+ try {
1785
+ const action = prepareSharedAction(ctx.store, ctx.who, "sweep_cancel_rest", { sweep: Number(args["sweep"]) }, ctx.evidenceRoot, ctx.now);
1786
+ const id = ctx.draft("action", { ...action });
1787
+ return id === null ? tooMany() : { ok: true, body: { proposal: id, label: action.title, awaiting: "human confirmation", executed: false } };
1788
+ }
1789
+ catch (error) {
1790
+ return { ok: false, message: error instanceof Error ? error.message : "That couldn't be drafted." };
1791
+ }
1792
+ },
1793
+ },
1643
1794
  propose_next: {
1644
1795
  description: "Move a task to its queue column front; intervening queue changes invalidate confirmation.",
1645
1796
  handle: (ctx, args) => {
package/dist/lead.d.ts CHANGED
@@ -27,6 +27,13 @@ export type LeadTurnInput = {
27
27
  queuedMessageId?: number;
28
28
  /** Called inside turn admission. A failed queue fence rolls admission back. */
29
29
  onAdmitted?: (turn: number) => void;
30
+ /** A personal queue claim (lead-queue.ts) this turn runs: with `queuedMessageId`, the queued message of the person's
31
+ * own thread this turn binds; admission lets it through the person's backlog (store.openLeadTurn). */
32
+ queueClaim?: {
33
+ message: number;
34
+ runner: string;
35
+ generation: number;
36
+ };
30
37
  /** Safe, server-authored context for this turn only. Kept out of the
31
38
  * visible thread so a task-scoped composer still reads like a normal
32
39
  * conversation. */
@@ -67,8 +74,8 @@ export type LeadTurnInput = {
67
74
  };
68
75
  /** Why a chat's standing changed under a turn: each has its own plain words (LEAD_CHANNEL_COPY). */
69
76
  export type LeadChannelProblem = "unpaired" | "not-approver" | "projects-changed" | "projects-unreadable" | "member-changed" | "access-changed";
70
- export type LeadRefusal = "empty-message" | "secret-in-message" | "secret-in-context" | "standing" | "unpriced" | "ceiling-changed" | "not-yours" | "thread-closed" | "latched" | "concurrent" | "daily-cap" | "session-exhausted" | "session-ended" | "over-budget" | "monthly-budget" | "policy" | "invalid-request" | "request-changed" | "channel";
71
- export type LeadFailure = "provider-error" | "timeout" | "malformed-reply" | "secret-refused" | "latched" | "revoked" | "superseded";
77
+ export type LeadRefusal = "empty-message" | "secret-in-message" | "secret-in-context" | "standing" | "unpriced" | "ceiling-changed" | "not-yours" | "thread-closed" | "latched" | "concurrent" | "backlog" | "daily-cap" | "session-exhausted" | "session-ended" | "over-budget" | "monthly-budget" | "policy" | "invalid-request" | "request-changed" | "channel";
78
+ export type LeadFailure = "provider-error" | "timeout" | "malformed-reply" | "secret-refused" | "latched" | "revoked" | "superseded" | "over-reservation";
72
79
  export type LeadTurnOutcome = {
73
80
  ok: true;
74
81
  replayed?: false;
@@ -115,16 +122,17 @@ export declare const LEAD_FAILURE_COPY: {
115
122
  readonly tooLongUnknownCost: string;
116
123
  readonly lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.";
117
124
  readonly unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.";
125
+ readonly overReserved: "This reply reported more use than was set aside for it, so its cost is counted as unknown and chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.";
118
126
  readonly empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.";
119
127
  readonly unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.";
120
128
  };
121
129
  /** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
122
- export declare function leadFailureText(reason: string | null): string;
130
+ export declare function leadFailureText(reason: string | null, unknownSpend?: boolean): string;
123
131
  /** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
124
132
  * Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
125
133
  export declare const LEAD_ABORT_GRACE_MS = 5000;
126
134
  /** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
127
- export declare function historyFor(store: Store, thread: number, queuedMessageId?: number): LeadHistoryMessage[];
135
+ export declare function historyFor(store: Store, thread: number, queuedMessageId?: number, named?: boolean): LeadHistoryMessage[];
128
136
  /** "read 3 · proposed 1 · 2 steps" — counts only, never results (ruling 11). */
129
- export declare function activitySummary(reads: number, proposals: number, steps: number): string;
137
+ export declare function activitySummary(reads: number, proposals: number, steps: number, cacheShare?: number | null): string;
130
138
  export declare function runLeadTurn(input: LeadTurnInput): Promise<LeadTurnOutcome>;