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
@@ -1,5 +1,9 @@
1
1
  /** Shared, exact-state actions. Models may prepare; only the existing human
2
2
  * confirmation door executes. Protected actions use a one-use review receipt. */
3
+ import { fileTaskProposal } from "./proposal.js";
4
+ import { heuristicSizing } from "./task-sizing.js";
5
+ import { cancelSweepRest, sweepOpen, sweepTable, VERDICT_RULE } from "./lead-sweep.js";
6
+ import { isAcpProfile } from "./acp-agents.js";
3
7
  import { withActor } from "./actor.js";
4
8
  import { TEXT_LIMITS } from "./text-limits.js";
5
9
  import { publicChatText } from "./chat-display.js";
@@ -9,7 +13,7 @@ import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
9
13
  import { isVerifiedApprover, reproveApprover, } from "./principal.js";
10
14
  import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
11
15
  import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
12
- import { approve, authenticateApprover, describeScope, scopeAuthorityOf, } from "./scope.js";
16
+ import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
13
17
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
14
18
  import { readAuthModeStrict } from "./keys.js";
15
19
  import { parseProof } from "./proof.js";
@@ -86,6 +90,11 @@ export const CHAT_ACTIONS = {
86
90
  flow_create: { label: "Create flow", protected: false, password: false },
87
91
  // A starter flow: its zones and its trigger, switched on with one yes (flow-starters.ts).
88
92
  flow_starter: { label: "Switch on", protected: false, password: false },
93
+ // Lead fan-out (docs/plans/lead-fan-out.md): files N tasks under one brief and approves each one's scope — the
94
+ // person's ceremony (protected, with the password), N ordinary approvals of exactly the scopes shown.
95
+ sweep_file: { label: "Start sweep", protected: true, password: true },
96
+ // Stops every item of a sweep not yet ready; finished results stay. An ordinary act, like cancelling a task.
97
+ sweep_cancel_rest: { label: "Cancel the rest", protected: false, password: false },
89
98
  flow_edit: { label: "Save flow", protected: false, password: false },
90
99
  flow_card_add: { label: "Add card", protected: false, password: false },
91
100
  flow_card_move: { label: "Move card", protected: false, password: false },
@@ -175,10 +184,12 @@ function runtimeTerms(profile) {
175
184
  ? profile.approvalArgv === "yolo"
176
185
  ? "Full access: every tool is approved automatically (--approval-mode yolo)."
177
186
  : "Edits are approved automatically; other tools are refused."
178
- : profile.sandboxMode === "danger-full-access"
179
- ? "Full access: approvals and sandbox are bypassed (--dangerously-bypass-approvals-and-sandbox)."
180
- : "Commands use the workspace-write sandbox.";
181
- return `${profile.provider} · ${profile.model}\n${permission}\n${profile.timeoutSeconds} seconds ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}${profile.provider === "claude" ? `; at most ${profile.maxTurns} turns` : ""}. Repairs: ${profile.repairModel}, ${profile.repairTimeoutSeconds} seconds${profile.provider === "claude" ? `, at most ${profile.repairMaxTurns} turns` : ""}.`;
187
+ : isAcpProfile(profile)
188
+ ? "Full access inside the OS fence: the agent runs any tool it chooses, and nothing outside the fence is reachable."
189
+ : profile.sandboxMode === "danger-full-access"
190
+ ? "Full access: approvals and sandbox are bypassed (--dangerously-bypass-approvals-and-sandbox)."
191
+ : "Commands use the workspace-write sandbox.";
192
+ return `${profile.provider} · ${profile.model}\n${permission}\n${isAcpProfile(profile) ? `${profile.timeoutSeconds} seconds per attempt, or ${profile.idleTimeoutSeconds} seconds silent` : `${profile.timeoutSeconds} seconds ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}`}${profile.provider === "claude" ? `; at most ${profile.maxTurns} turns` : ""}. Repairs: ${profile.repairModel}, ${profile.repairTimeoutSeconds} seconds${profile.provider === "claude" ? `, at most ${profile.repairMaxTurns} turns` : ""}.`;
182
193
  }
183
194
  function resultOf(store, task, run, repo) {
184
195
  const ref = store.lookupRef(task);
@@ -263,7 +274,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
263
274
  const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
264
275
  return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
265
276
  }
266
- const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_")
277
+ const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_")
267
278
  ? null
268
279
  : text(input, "task", 64);
269
280
  const flowTarget = operation.startsWith("flow_") && operation !== "flow_create" && operation !== "flow_script_save" && operation !== "flow_starter" ? flowTargetOf(store, input) : null;
@@ -275,7 +286,8 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
275
286
  const questionFlow = questionTarget === null ? null : store.getFlow(store.getFlowCard(questionTarget.card)?.flow ?? -1);
276
287
  if (operation === "subagent_answer" && (questionTarget === null || questionFlow === null))
277
288
  throw Error("That question isn't open any more.");
278
- const repo = flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
289
+ const repo = operation === "sweep_cancel_rest" ? store.getSweep(integer(input, "sweep"))?.repo
290
+ : flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
279
291
  if (!repo)
280
292
  throw Error("Choose an available project.");
281
293
  requireActor(store, who, repo);
@@ -554,6 +566,50 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
554
566
  terms.push(question.question, `Your answer: ${[choice?.label, said].filter(Boolean).join(" — ")}`);
555
567
  }
556
568
  }
569
+ else if (operation === "sweep_cancel_rest") {
570
+ // Exactly the items open now: if any settles or starts in between, the card is stale and a fresh one is needed.
571
+ const table = sweepTable(store, integer(input, "sweep"), now, who.repos);
572
+ if (table === null || table.repo !== repo)
573
+ throw Error("That sweep isn't one of yours.");
574
+ // Exactly the items as they stand now — each one's active task (a revision, after one), state, running attempt and
575
+ // last change: any change makes this card stale, so nothing is acted on that wasn't shown.
576
+ const open = sweepOpen(store, table, now);
577
+ if (open.length === 0)
578
+ throw Error("Nothing in that sweep is still unfinished.");
579
+ state = { open };
580
+ title = `Cancel the rest of ${table.title}`;
581
+ const running = open.filter(one => one.run !== null);
582
+ terms.push(`Cancels ${open.length - running.length} unfinished item${open.length - running.length === 1 ? "" : "s"}${running.length === 0 ? "" : ` and stops ${running.length} running build${running.length === 1 ? "" : "s"}`}: ${open.map(one => `${one.label} (${one.task})`).join(", ")}.`, "Finished results stay.");
583
+ }
584
+ else if (operation === "sweep_file") {
585
+ // Everything the person approves is on this screen: the shared rules, every item, the one success test, the
586
+ // limit. Confirming files exactly these and approves exactly these scopes; anything changed is a new card.
587
+ const sweepTitle = text(input, "title", 200).trim(), brief = text(input, "brief", 8000).trim();
588
+ const items = Array.isArray(input["items"]) ? input["items"] : [];
589
+ if (items.length < 2 || items.length > 20)
590
+ throw Error("A sweep is 2 to 20 items. For one, propose a task.");
591
+ const cleaned = items.map((one, index) => ({ title: text(one, "title", 200).trim(), item: text(one, "item", 4000).trim(), index }));
592
+ const maxParallel = integer(input, "maxParallel");
593
+ if (maxParallel < 1 || maxParallel > 8)
594
+ throw Error("Run 1 to 8 at once.");
595
+ request["title"] = sweepTitle;
596
+ request["brief"] = brief;
597
+ request["items"] = cleaned.map(({ title, item }) => ({ title, item }));
598
+ const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
599
+ title = `Start a sweep in ${project}: ${sweepTitle}`;
600
+ // Every word each child's scope will hold is shown here (the security review: nothing approved off-screen): its
601
+ // goal is the shared rules, then its item, then the verdict line; its success test is the shared acceptance.
602
+ const acceptance = Array.isArray(input["acceptance"]) ? input["acceptance"] : [];
603
+ if (acceptance.length === 0)
604
+ throw Error("A sweep needs a success test every item shares.");
605
+ // Every child's scope exactly as filing would make it — its words AND its agent's permissions, limits and route —
606
+ // found by filing them in a savepoint that is always rolled back (prepare writes nothing). They are on this card and
607
+ // in its state: if a project default changes before confirm, the card is stale and nothing is filed or approved.
608
+ const children = sweepChildScopes(store, who, repo, brief, cleaned.map(({ title, item }) => ({ title, item })), input["acceptance"], now);
609
+ state = { children: children.map(one => one.fingerprint) };
610
+ const tests = acceptance.map((one, index) => `Success test ${index + 1}${typeof one["id"] === "string" ? ` (${one["id"]})` : ""}: ${String(one["statement"] ?? "")}${Array.isArray(one["evidence"]) ? ` — shown by ${one["evidence"].map(String).join(", ")}` : ""}`);
611
+ terms.push(`${cleaned.length} tasks, each in its own worktree on its own branch, at most ${maxParallel} running at once, built directly (no separate planning step).`, `Shared rules for every item: ${brief}`, ...cleaned.map(one => `Item ${one.index + 1} — ${one.title}: ${one.item}`), `Each item's goal is the shared rules, then its own item, then: ${VERDICT_RULE}`, ...tests.map(line => `Every item — ${line}`), ...(children.every(one => one.runtime === children[0].runtime) ? [`Every item — ${children[0].runtime}`] : children.map((one, index) => `Item ${index + 1} — ${one.runtime}`)), "Confirming files all of them and approves each one's scope exactly as shown. An item that needs a second person, or that the project's rules keep from its requester, is filed and waits for its own approval.", "Each ends with a verdict (fixed, not reproducible, blocked or done) and the sweep's table shows them as they land.");
612
+ }
557
613
  else if (operation.startsWith("flow_")) {
558
614
  const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
559
615
  if (operation === "flow_create") {
@@ -985,10 +1041,28 @@ function savedActionContext(store, who, id, state) {
985
1041
  requireActor(store, who, payload.repo, payload.operation);
986
1042
  return payload;
987
1043
  }
1044
+ /** An action's terms alone, without whose proposal it was (actor, generation, ceiling). */
1045
+ const termsStampOf = (action) => hash({ operation: action.operation, request: action.request, repo: action.repo, state: action.state, terms: action.terms });
1046
+ /**
1047
+ * Whether the live action is the saved one: the exact stamp; or, for a scope approval already holding another
1048
+ * approver's yes on these exact terms (awaiting the second), the same terms — each reviewer's own nonce and authority
1049
+ * are still bound to them individually.
1050
+ */
1051
+ function sameSharedAction(store, who, live, saved) {
1052
+ if (live.stamp === saved.stamp)
1053
+ return true;
1054
+ if (saved.operation !== "scope_approve" || termsStampOf(live) !== termsStampOf(saved))
1055
+ return false;
1056
+ const task = typeof saved.request["task"] === "string" ? saved.request["task"] : null;
1057
+ const scope = task === null ? null : store.getScope(task);
1058
+ if (scope === null)
1059
+ return false;
1060
+ return store.handle.prepare("SELECT 1 FROM scope_approval_vote WHERE task_id = ? AND digest = ? AND approver <> ? LIMIT 1").get(task, scope.digest, who.name) !== undefined;
1061
+ }
988
1062
  export function mintSharedActionReview(store, who, id, root, now) {
989
1063
  const payload = savedActionContext(store, who, id, "pending");
990
1064
  const live = prepareSharedAction(store, who, payload.operation, payload.request, root, now);
991
- if (live.stamp !== payload.stamp)
1065
+ if (!sameSharedAction(store, who, live, payload))
992
1066
  throw Error("This action changed. Ask for a fresh proposal.");
993
1067
  const nonce = randomBytes(24).toString("hex");
994
1068
  if (!store.mintCeremonyNonce({
@@ -1014,7 +1088,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1014
1088
  if (hash(saved) !== hash(payload))
1015
1089
  return refuse("refused", "This action does not match the saved proposal.");
1016
1090
  live = prepareSharedAction(store, who, saved.operation, saved.request, options.root, now);
1017
- if (live.stamp !== saved.stamp)
1091
+ if (!sameSharedAction(store, who, live, saved))
1018
1092
  return refuse("stale", "This action changed. Ask for a fresh proposal.");
1019
1093
  }
1020
1094
  catch (error) {
@@ -1022,6 +1096,8 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1022
1096
  ? error.message
1023
1097
  : "This action is no longer available.");
1024
1098
  }
1099
+ // The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
1100
+ const savedStamp = payload.stamp;
1025
1101
  payload = live;
1026
1102
  const config = CHAT_ACTIONS[live.operation], req = live.request;
1027
1103
  const challenged = options.via !== "web" &&
@@ -1031,14 +1107,21 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1031
1107
  if (sharedActionNeedsReview(payload) && !challenged) {
1032
1108
  if (options.via !== "web" || !options.review || options.confirm !== true)
1033
1109
  return refuse("needs-confirm", "Review the complete action in the secure confirmation screen.");
1034
- if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id, payload.stamp, now))
1110
+ if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id, savedStamp, now))
1035
1111
  return refuse("needs-confirm", "This confirmation expired or was already used. Open the review again.");
1112
+ // A scope approval proves its person inside approve() (one transaction with the seal, under the same freshness
1113
+ // rule as the task page); here an empty field is only looked at, never spent (an explicit re-check is one-use).
1114
+ const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
1036
1115
  if (config.password &&
1037
- !authenticateApprover(store, who.name, options.review.password, payload.repo).ok)
1116
+ (emptyApproval
1117
+ ? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
1118
+ : !authenticateApprover(store, who.name, options.review.password, payload.repo).ok))
1038
1119
  return refuse("needs-confirm", "Confirm with your password in the secure screen.");
1039
1120
  }
1040
1121
  try {
1041
- return store.savepoint(() => {
1122
+ // A scope approval's re-check (sso-now) is spent only if it happens: a refusal or a rolled-back savepoint puts it
1123
+ // back (its every effect is in this transaction). Other actions spend it whatever comes of them.
1124
+ const act = () => store.savepoint(() => {
1042
1125
  const task = typeof req["task"] === "string" ? req["task"] : null, repo = payload.repo, actor = who.name;
1043
1126
  let taskId = task;
1044
1127
  if (payload.operation === "lead_about_you") {
@@ -1076,6 +1159,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1076
1159
  const done = runFlowAction(store, payload, actor, who.repos, options.root, now);
1077
1160
  return { ok: true, taskId: null, said: done.said, href: done.href };
1078
1161
  }
1162
+ else if (payload.operation === "sweep_cancel_rest") {
1163
+ const said = cancelSweepRest(store, who.name, payload.state["open"], now, "web");
1164
+ return { ok: true, taskId: null, said, href: `/sweeps/${Number(req["sweep"])}` };
1165
+ }
1166
+ else if (payload.operation === "sweep_file") {
1167
+ const done = fileSweep(store, who, id, payload, options.review?.password ?? "", now);
1168
+ return { ok: true, taskId: null, said: done.said, href: done.href };
1169
+ }
1079
1170
  else if (payload.operation === "kit_setup") {
1080
1171
  const kit = kitOf(String(req["kit"]));
1081
1172
  const made = setUpKitNow(store, kit, repo, actor, now, null, true);
@@ -1117,11 +1208,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1117
1208
  }, now);
1118
1209
  else if (payload.operation === "scope_approve") {
1119
1210
  const scope = store.getScope(task);
1120
- const result = approve(store, task, actor, now, scope.digest, options.review.password);
1211
+ // Cockpit B2: classified by the act, not the road — the same standard-approval freshness as /t/:id/approve.
1212
+ const result = withFreshness("approve-standard", () => approve(store, task, actor, now, scope.digest, options.review.password));
1121
1213
  if (!result.ok) {
1122
- // v102: the project's approval rules speak for themselves.
1214
+ // A first yes on protected work is kept, with how its person was proved: committed, never rolled back.
1215
+ // The card stays open (`awaiting`) for the second approver; the same person again changes nothing.
1123
1216
  if (result.reason === "second-approver")
1124
- throw Error(gateWords({ verdict: "vote", have: result.have, need: 2, already: result.already }));
1217
+ return { ok: true, taskId, awaiting: true, ...(result.already ? { already: true } : {}), said: gateWords({ verdict: "vote", have: result.have, need: 2, already: result.already }) };
1218
+ // v102: the project's approval rules speak for themselves.
1125
1219
  if (result.reason === "requester" || result.reason === "person-required")
1126
1220
  throw Error(gateWords({ verdict: "refuse", reason: result.reason }));
1127
1221
  if (result.reason === "policy")
@@ -1187,6 +1281,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1187
1281
  : "Project knowledge saved.";
1188
1282
  return { ok: true, taskId, said };
1189
1283
  });
1284
+ return payload.operation === "scope_approve" ? spendingOnlyOnSuccess(act, result => !result.ok || ("already" in result && result.already === true)) : act();
1190
1285
  }
1191
1286
  catch (error) {
1192
1287
  return refuse("refused", error instanceof Error
@@ -1195,6 +1290,93 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1195
1290
  }
1196
1291
  }
1197
1292
  /** A confirmed subagent action (v92), through the same helpers the Subagents pages use. */
1293
+ /**
1294
+ * Lead fan-out: file every item (all of them or none — any refusal throws and the savepoint takes the whole sweep
1295
+ * back), link them to one sweep record, then approve each one's scope through the ordinary approval (its own digest,
1296
+ * its own rules). A child the approval refuses stays filed and unapproved; it is named, never retried.
1297
+ */
1298
+ /** One child as filing makes it (the shared brief, its item, the verdict rule; the shared acceptance; built directly). */
1299
+ function fileSweepChild(store, who, repo, brief, one, acceptance, now) {
1300
+ const goal = `${brief}\n\nThis item:\n${one.item}\n\n${VERDICT_RULE}`;
1301
+ return fileTaskProposal(store, {
1302
+ title: one.title, repo, goal, acceptance, planning: "skip",
1303
+ // Sized now, by the same heuristic filing would use: no background classifier is started — the card's dry run must
1304
+ // leave nothing running after its rollback, and the approved scope must be the one shown (Codex sweep r2).
1305
+ sizing: heuristicSizing({ title: one.title, goal, outOfScope: null }),
1306
+ filedVia: "mate", filedBy: { name: who.name, kind: "person" }, proposedVia: "mate", admittedRepos: [...who.repos],
1307
+ }, now);
1308
+ }
1309
+ /** A filed child's scope in the words an approval agrees to (no identity lines), its runtime terms, and their digest. */
1310
+ function sweepChildScopeOf(store, taskId) {
1311
+ const scope = store.getScope(taskId);
1312
+ if (scope === null)
1313
+ throw Error("A sweep item has no scope to show.");
1314
+ const authority = scopeAuthorityOf(scope, { authMode: readAuthModeStrict });
1315
+ if (!authority.ok)
1316
+ throw Error(authority.problem);
1317
+ const words = describeScope(scope).filter(line => !/^\s*(reference|approved)\s/.test(line)).map(line => line.trim());
1318
+ const runtime = `Agent permissions and limits: ${runtimeTerms(authority.profile)} ${authority.authMode === "api-key" ? "Uses your API key; spend is charged to that account." : authority.authMode === "subscription" ? "Uses your subscription login." : ""}`.trim();
1319
+ // The fingerprint covers the scope's every field, not only what is rendered (a profile setting no sentence names, such
1320
+ // as an agent's idle timeout, still makes the card stale): its exact terms with only identity left out.
1321
+ const { taskId: _task, digest: _digest, approvedDigest: _approved, approvedBy: _by, approvedAt: _at, proposedAt: _proposed, approvalBasis: _basis, modeDigest: _mode, approvedProfile: _approvedProfile, approvedProfileJson: _approvedProfileJson, approvedChainJson: _approvedChain, ...terms } = scope;
1322
+ return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words };
1323
+ }
1324
+ /** Every child as filing would make it, found in a savepoint that is always rolled back: prepare never writes. */
1325
+ function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
1326
+ const dryRun = new Error("sweep dry run");
1327
+ let found = [];
1328
+ // Inside one transaction (filing opens its own; transact flattens), a savepoint that always rolls back.
1329
+ store.transact(() => {
1330
+ try {
1331
+ store.savepoint(() => {
1332
+ found = items.map((one, index) => {
1333
+ const made = fileSweepChild(store, who, repo, brief, one, acceptance, now);
1334
+ if (!made.ok)
1335
+ throw Error(`Item ${index + 1} (${one.title}) can't be filed: ${made.message}`);
1336
+ return sweepChildScopeOf(store, made.id);
1337
+ });
1338
+ throw dryRun;
1339
+ });
1340
+ }
1341
+ catch (error) {
1342
+ if (error !== dryRun)
1343
+ throw error;
1344
+ }
1345
+ });
1346
+ return found;
1347
+ }
1348
+ function fileSweep(store, who, proposal, payload, password, now) {
1349
+ const req = payload.request;
1350
+ const already = store.sweepOfProposal(proposal);
1351
+ if (already !== null)
1352
+ return { said: "This sweep was already started.", href: `/sweeps/${already}` };
1353
+ const items = req["items"];
1354
+ const brief = String(req["brief"]);
1355
+ const sweep = store.createSweep({ owner: who.name, thread: store.getLeadProposal(proposal)?.thread ?? null, repo: payload.repo, title: String(req["title"]), brief,
1356
+ maxParallel: Number(req["maxParallel"]), proposal }, now);
1357
+ const filed = [];
1358
+ for (const [index, one] of items.entries()) {
1359
+ const made = fileSweepChild(store, who, payload.repo, brief, one, req["acceptance"], now);
1360
+ if (!made.ok)
1361
+ throw Error(`Item ${index + 1} (${one.title}) couldn't be filed: ${made.message}. Nothing was filed.`);
1362
+ // Each filed scope must be exactly the one the card showed (words, permissions, limits, route); else nothing is filed.
1363
+ if (sweepChildScopeOf(store, made.id).fingerprint !== payload.state["children"]?.[index])
1364
+ throw Error(`Item ${index + 1} (${one.title}) no longer matches what you reviewed (a project default changed). Nothing was filed; ask for a fresh card.`);
1365
+ store.addSweepTask(sweep, made.id, index, one.title);
1366
+ filed.push({ id: made.id, title: one.title });
1367
+ }
1368
+ const waiting = [];
1369
+ for (const one of filed) {
1370
+ const scope = store.getScope(one.id);
1371
+ const approved = scope === null ? null : withFreshness("approve-standard", () => approve(store, one.id, who.name, now, scope.digest, password));
1372
+ if (approved === null || !approved.ok)
1373
+ waiting.push(one.title);
1374
+ }
1375
+ const said = waiting.length === 0
1376
+ ? `Started: ${filed.length} tasks, at most ${Number(req["maxParallel"])} at once.`
1377
+ : `Started: ${filed.length - waiting.length} of ${filed.length} approved; ${waiting.length} wait for their own approval (${waiting.join(", ")}).`;
1378
+ return { said, href: `/sweeps/${sweep}` };
1379
+ }
1198
1380
  function runSubagentAction(store, payload, actor, now) {
1199
1381
  const req = payload.request;
1200
1382
  if (payload.operation === "subagent_create") {
@@ -12,6 +12,10 @@
12
12
  */
13
13
  import type { ChatBinding, ChatEvent, ChatState } from "./chat-delivery-state.js";
14
14
  import { type LeadAsk, type Store } from "./store.js";
15
+ /** Whether a task (the execution shown, never only its family's root) moved past a lead's question asked at `since`:
16
+ * its plan was rewritten or approved, or a new attempt of it started. The options answered a state that is gone. A run
17
+ * that merely finishes, or a review beside it, answers nothing, so it never takes the question's buttons. */
18
+ export declare function taskMovedSince(store: Store, taskId: string, since: string): boolean;
15
19
  /** The question as text with its options, where no buttons are drawn. */
16
20
  export declare function chatAskText(ask: Pick<LeadAsk, "question" | "options">): string;
17
21
  /** The live buttons on one part, in the order they were minted: the options, then "Something else". None when the part
package/dist/chat-ask.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { chatHash, partContent, savedChatPart } from "./chat-delivery-state.js";
2
2
  import { LEAD_ASK_OTHER } from "./store.js";
3
+ /** Whether a task (the execution shown, never only its family's root) moved past a lead's question asked at `since`:
4
+ * its plan was rewritten or approved, or a new attempt of it started. The options answered a state that is gone. A run
5
+ * that merely finishes, or a review beside it, answers nothing, so it never takes the question's buttons. */
6
+ export function taskMovedSince(store, taskId, since) {
7
+ const at = new Date(since).getTime();
8
+ if (Number.isNaN(at))
9
+ return false;
10
+ const after = (iso) => iso != null && new Date(iso).getTime() > at;
11
+ const scope = store.getScope(taskId);
12
+ if (scope !== null && (after(scope.proposedAt) || after(scope.approvedAt)))
13
+ return true;
14
+ const ref = store.lookupRef(taskId);
15
+ return ref !== null && store.runsFor(ref.id).some(run => run.role !== "reviewer" && after(run.startedAt));
16
+ }
3
17
  /** The question as text with its options, where no buttons are drawn. */
4
18
  export function chatAskText(ask) {
5
19
  return [ask.question, ...ask.options.map(one => `• ${one}`), `• ${LEAD_ASK_OTHER}: say it in your own words`].join("\n");
@@ -509,6 +509,11 @@ export const CHAT_ACTION_PARITY = {
509
509
  how: "Read by the model during a phone turn over the enrolled ceiling.",
510
510
  gap: null,
511
511
  },
512
+ find_tools: {
513
+ support: "direct",
514
+ how: "Searches the lead's own other tools by what it wants to do; what it finds is offered to it from its next step in the same reply. Same on every channel: the shared engine runs it.",
515
+ gap: null,
516
+ },
512
517
  list_repos: {
513
518
  support: "direct",
514
519
  how: "Read during a turn; projects are r1..rN in enrollment order, as on the console.",
@@ -583,6 +588,21 @@ export const CHAT_ACTION_PARITY = {
583
588
  how: "Card with Confirm/Dismiss through confirmLeadProposal (filed as a lead proposal, via telegram). The confirmed card links Review & start for the filed task while its scope waits; under a signed automatic mode it says the scope is approved and links the task.",
584
589
  gap: "Under manual approval the password step happens in the console, reached from the card's button.",
585
590
  },
591
+ propose_sweep: {
592
+ support: "direct",
593
+ how: "A protected shared action (sweep_file): the card lists every item, the shared rules and the success test; confirming files them all and approves each scope.",
594
+ gap: "Starting a sweep approves work, so it opens the secure review in the console (the password, or Passwordless).",
595
+ },
596
+ get_sweep: {
597
+ support: "direct",
598
+ how: "Reads a sweep's table (each item's state, verdict and result) from the local database through the shared engine.",
599
+ gap: null,
600
+ },
601
+ propose_sweep_cancel: {
602
+ support: "direct",
603
+ how: "An ordinary shared action (sweep_cancel_rest): Confirm cancels every item not yet ready; finished results stay.",
604
+ gap: null,
605
+ },
586
606
  propose_scope: {
587
607
  support: "direct",
588
608
  how: "Confirm rewrites the scope through the shared door; the confirmed card links Review & start for the exact task.",
@@ -63,7 +63,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
63
63
  var carry=JSON.parse(sessionStorage.getItem(carryKey)||'null');sessionStorage.removeItem(carryKey);
64
64
  if(!draft&&carry&&user&&carry.owner===user&&carry.task===task&&typeof carry.text==='string'&&carry.text.length<=2000&&typeof carry.at==='number'&&carry.at>Date.now()-86400000){box.value=carry.text;draft={text:carry.text,request:request.value,submitted:false,at:Date.now()};save();}
65
65
  }catch(e){storage=false;}
66
- function buttons(){document.querySelectorAll('form[action="/chat"] button[type="submit"]').forEach(function(button){button.disabled=busy||sending||stale||offline;});}
66
+ function buttons(){document.querySelectorAll('form[action="/chat"] button[type="submit"]').forEach(function(button){button.disabled=sending||stale||offline;});}
67
67
  function showReconnect(on){if(reconnect)reconnect.hidden=!on;}
68
68
  function lostAuthority(){stale=true;buttons();showReconnect(true);say('Sign in again to reconnect. Your draft stays in this tab.');}
69
69
  function changed(){stale=true;buttons();showReconnect(true);say('This conversation changed or ended. Your draft stays in this tab; reconnect to continue.');}
@@ -97,7 +97,8 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
97
97
  if(!(one instanceof HTMLFormElement))return;
98
98
  var action=one.getAttribute('action')||'';
99
99
  if(action==='/chat'){
100
- if(busy||sending||stale||offline){event.preventDefault();return;}
100
+ // A reply in progress no longer holds the composer: a message sent now waits its turn (the lead queue).
101
+ if(sending||stale||offline){event.preventDefault();return;}
101
102
  bind(one);
102
103
  if(one===form){
103
104
  if(!box.value.trim()){event.preventDefault();return;}
@@ -125,7 +126,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
125
126
  var data=got.data||{};
126
127
  if(data.ok===true){
127
128
  if(String(data.session)!==session){changed();return;}
128
- say('Sent. Waiting for the reply…');later(300);return;
129
+ say(data.queued===true?'Queued. It goes to the lead after the reply in progress.':'Sent. Waiting for the reply…');later(300);return;
129
130
  }
130
131
  // Refused, so nothing was received: the draft stays, editable and
131
132
  // resendable under its own key; the words are the server's.
@@ -302,7 +303,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
302
303
  }
303
304
  settleLive();
304
305
  busy=data.pending;buttons();
305
- say(busy?'Reply in progress. You can draft your next message or come back later.':sent?'Message not confirmed. Check the conversation before retrying.':'');
306
+ say(busy?'Reply in progress. A message you send now waits its turn.':sent?'Message not confirmed. Check the conversation before retrying.':'');
306
307
  later(busy?2500:5000);
307
308
  }catch(e){say('Connection lost. Reconnecting… Your existing work is not cancelled.');later(10000);}
308
309
  finally{polling=false;}
@@ -1,17 +1,3 @@
1
- /** Decisions finish in the chat app. A pushed card about a result, a failed task, a plan waiting for approval or a
2
- * pull request ready to merge carries buttons that act in place: Accept and finish, Request changes, Retry,
3
- * Approve & start, Merge. Every act that changes something asks twice ("Accept and finish 'X'? [Yes] [Cancel]"),
4
- * and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
5
- * newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
6
- *
7
- * Approving a plan or merging needs the password, which never goes in chat, so they act in chat only where the owner
8
- * turned approving from chat on (their lasting setting, chat-approval.ts, or a signed mode's chatApprove term), for
9
- * their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
10
- * card keeps its link.
11
- *
12
- * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
13
- * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
14
- * console uses. */
15
1
  import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
16
2
  import { type ChatApprovalLimits } from "./chat-approval.js";
17
3
  import { type VerifiedApprover } from "./principal.js";
@@ -12,6 +12,7 @@
12
12
  * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
13
13
  * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
14
14
  * console uses. */
15
+ import { isAcpProfile } from "./acp-agents.js";
15
16
  import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
16
17
  import { createHash, randomBytes } from "node:crypto";
17
18
  import { acceptAndCompleteAsOperator, assignmentOf, owedAcceptance } from "./assignment.js";
@@ -81,13 +82,15 @@ export function permissionPlainWords(profile) {
81
82
  }
82
83
  if (profile.provider === "gemini")
83
84
  return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
85
+ if (isAcpProfile(profile))
86
+ return "full access inside the OS fence, nothing asks first";
84
87
  return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
85
88
  }
86
89
  function fullAccess(profile) {
87
90
  if (profile === null || profile === undefined)
88
91
  return false;
89
92
  return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
90
- : profile.provider === "gemini" ? profile.approvalArgv === "yolo" : profile.sandboxMode === "danger-full-access";
93
+ : profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
91
94
  }
92
95
  const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
93
96
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
@@ -3,6 +3,15 @@ import { ChatState, type ChatBinding, type ChatContent, type ChatEvent, type Cha
3
3
  import { type Store } from "./store.js";
4
4
  import type { SubscriptionLeadRunner } from "./subscription-chat.js";
5
5
  import { type ChatMerge } from "./chat-decide.js";
6
+ /**
7
+ * The watch at a chat app's final fence (cockpit §3, R11), right before the network: a notice about an agent its
8
+ * person has open now waits, as a held part, until the watch ends; then it is delivered as usual. The subject is the
9
+ * part's own run, or its task's live build. Only a part planned with `watch` (never a security alert or the digest)
10
+ * can be held. True: held, so nothing is sent now.
11
+ */
12
+ export declare function heldForWatch(state: ChatState, store: Store, part: number, event: {
13
+ kind: string;
14
+ }, approver: string, content: ChatContent, now: Date): boolean;
6
15
  export declare const chatObject: (v: unknown) => Record<string, unknown>;
7
16
  export type ChatDeliveryOptions = {
8
17
  store: Store;
@@ -26,7 +26,33 @@ import { isShotsKind, resultShotsFor } from "./result-shots.js";
26
26
  import { CHAT_APP_NAMES } from "./flow-triggers.js";
27
27
  import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
28
28
  import { mergePullRequest } from "./pull-request-flow.js";
29
- import { watchedPing } from "./agent-board.js";
29
+ import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
30
+ /**
31
+ * The watch at a chat app's final fence (cockpit §3, R11), right before the network: a notice about an agent its
32
+ * person has open now waits, as a held part, until the watch ends; then it is delivered as usual. The subject is the
33
+ * part's own run, or its task's live build. Only a part planned with `watch` (never a security alert or the digest)
34
+ * can be held. True: held, so nothing is sent now.
35
+ */
36
+ export function heldForWatch(state, store, part, event, approver, content, now) {
37
+ // Fail closed: only a notice planned as holdable (a kind a watch may hold) is ever held here.
38
+ if (event.kind !== "notice" || content.watch !== true)
39
+ return false;
40
+ // One part may cover several tasks (a grouped card, a batch): it waits only while every one of them is watched,
41
+ // until the first of those watches ends.
42
+ const subjects = content.subjects ?? [{ ref: content.task ? store.lookupRef(content.task)?.id ?? null : null, run: content.run ?? null }];
43
+ if (subjects.length === 0)
44
+ return false;
45
+ let until = null;
46
+ for (const subject of subjects) {
47
+ const decided = watchedPing(store, { kind: "notice", run: subject.run, taskRef: subject.ref }, approver, now);
48
+ if (decided === "send" || decided === "skip")
49
+ return false;
50
+ if (until === null || decided.hold < until)
51
+ until = decided.hold;
52
+ }
53
+ state.holdPart(part, until);
54
+ return true;
55
+ }
30
56
  export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
31
57
  ? v
32
58
  : {};
@@ -397,8 +423,9 @@ export async function processChatEvent(options) {
397
423
  return false;
398
424
  if (!outcome.ok &&
399
425
  "refused" in outcome &&
400
- outcome.refused === "concurrent") {
401
- state.defer(event.id, "Assistant is answering another message", new Date(nowOf(options).getTime() + 5000));
426
+ (outcome.refused === "concurrent" || outcome.refused === "backlog")) {
427
+ // The lead is answering, or messages sent earlier wait in the person's lead queue: this one waits its turn.
428
+ state.defer(event.id, outcome.refused === "backlog" ? "Waiting behind earlier messages" : "Assistant is answering another message", new Date(nowOf(options).getTime() + 5000));
402
429
  return true;
403
430
  }
404
431
  receipt = store.leadRequestReceipt(resolved.session.id, request);
@@ -422,7 +449,7 @@ export async function processChatEvent(options) {
422
449
  if (turn?.state !== "answered") {
423
450
  state.plan(event.id, [
424
451
  {
425
- text: leadFailureText(turn?.failureReason ?? null),
452
+ text: leadFailureText(turn?.failureReason ?? null, turn != null && store.leadTurnCostUnconfirmed(turn.id)),
426
453
  },
427
454
  ], nowOf(options));
428
455
  return true;
@@ -734,7 +761,12 @@ function planQuietCard(options, binding, notification, now) {
734
761
  const view = quietCardView(store, card.tasks, now, options.evidenceRoot, binding.approver);
735
762
  if (view === null)
736
763
  return;
737
- const content = { text: view.text, link: view.link, ...(card.tasks.length === 1 ? { task: notification.taskId } : {}) };
764
+ // A card about one task is that task's: its person's watch may hold it at the final fence (never a security kind).
765
+ // A card about its tasks is theirs: its person's watch may hold it at the final fence while every one of them is
766
+ // watched (never for a security kind).
767
+ const content = { text: view.text, link: view.link, ...(card.tasks.length === 1 ? { task: notification.taskId } : {}),
768
+ ...(card.tasks.length > 0 && !neverHeld(notification.kind) ? { watch: true } : {}),
769
+ ...(card.tasks.length > 1 && !neverHeld(notification.kind) ? { subjects: card.tasks.slice(0, 100).map(ref => ({ ref, run: null })) } : {}) };
738
770
  const shown = chatHash(JSON.stringify(content));
739
771
  if (card.message !== null) {
740
772
  if (card.digest === shown)
@@ -763,7 +795,12 @@ function planFinished(options, binding, taskRef, run, now, notification = null,
763
795
  // A single update keeps its task and run, so a reply to it names that work.
764
796
  const single = batch.items.length === 1 ? batch.items[0] : null;
765
797
  const task = single === null ? null : store.refById(single.taskRef)?.externalId ?? null;
766
- const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }) };
798
+ // Holdable when its fact is a kind a watch may hold (the batch line for a finished run, or the notification joining
799
+ // it); a batch of several holds only while every one of them is watched.
800
+ const joining = notification === null ? null : options.store.notificationById(notification);
801
+ const holdable = batch.items.length > 0 && (joining === null || !neverHeld(joining.kind));
802
+ const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }), ...(holdable ? { watch: true } : {}),
803
+ ...(holdable && batch.items.length > 1 ? { subjects: batch.items.slice(0, 100).map(one => ({ ref: one.taskRef, run: one.run })) } : {}) };
767
804
  // A message about one result, plan, failure or pull request acts in place (chat-decide.ts), as on Telegram: its
768
805
  // buttons are minted only when what it offers changed, or its own were spent (Not now), so an unchanged repaint keeps them.
769
806
  const offer = decideOfferFor(options, binding, view.target, repos, now);
@@ -962,6 +999,7 @@ function offerNotice(options, notification, binding, registry) {
962
999
  task: notification.taskId,
963
1000
  run: run.id,
964
1001
  link: card.link,
1002
+ ...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
965
1003
  };
966
1004
  const digest = chatHash(JSON.stringify(content));
967
1005
  const prior = state
@@ -1044,6 +1082,7 @@ function offerNotice(options, notification, binding, registry) {
1044
1082
  text: offer?.text ?? chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
1045
1083
  ...(notification.taskId ? { task: notification.taskId } : {}),
1046
1084
  ...(run ? { run: run.id } : {}),
1085
+ ...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
1047
1086
  ...(offer !== null ? { decide: mintDecide(options, binding, target, offer, now) }
1048
1087
  : fallback !== null ? { link: fallback }
1049
1088
  : notification.link
@@ -174,6 +174,8 @@ export declare class ChatMessages extends ChatCore {
174
174
  taskId: string | null;
175
175
  sourceRun: number | null;
176
176
  }, now: Date): number;
177
+ /** How many of this chat's messages wait for, or are being answered by, the lead (the lead queue's bound per chat). */
178
+ messagesWaiting(binding: number): number;
177
179
  /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
178
180
  messageWaitingOn(binding: number, messageId: string): boolean;
179
181
  getMessage(id: number): TelegramConversation | null;
@@ -294,6 +296,12 @@ export declare class ChatMessages extends ChatCore {
294
296
  claimDeliveries(binding: TelegramBinding, owner: string, ttlMs: number, now: Date, only?: "all" | "urgent"): TelegramDelivery[];
295
297
  /** Synchronous final fence, called AFTER each asynchronous enrollment read. */
296
298
  deliveryProblem(row: TelegramDelivery, binding: TelegramBinding, owner: string, projects: readonly string[], now: Date, batch?: readonly number[]): string | null;
299
+ /**
300
+ * The watch at the final fence (cockpit §3, R11): right before the network, a person who has since opened the
301
+ * agent this ping is about holds it — the claim is released with the watch's end as its next attempt, no attempt
302
+ * spent — and news about a finished run they have now seen settles quietly. "send": nothing changed.
303
+ */
304
+ watchFence(row: TelegramDelivery, binding: TelegramBinding, owner: string, now: Date): "send" | "held" | "skipped";
297
305
  claimHeld(row: TelegramDelivery, owner: string, now: Date): boolean;
298
306
  /** A confirmed network message remains history even if authority changed
299
307
  * while it was in flight. It only binds the OLD destination, never new work. */