toolroll 0.9.93 → 0.9.95

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 (436) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +10 -0
  5. package/dist/agent-fence.js +6 -3
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -1
  12. package/dist/assignment.js +5 -0
  13. package/dist/attest.d.ts +3 -0
  14. package/dist/attest.js +10 -0
  15. package/dist/boot-identity.d.ts +2 -1
  16. package/dist/boot-identity.js +6 -4
  17. package/dist/bounded-spawn.d.ts +57 -0
  18. package/dist/bounded-spawn.js +220 -0
  19. package/dist/browser/terminal.js +42 -42
  20. package/dist/browser/workspace.css +1 -1
  21. package/dist/browser/workspace.js +32 -32
  22. package/dist/browser-crew.js +2 -0
  23. package/dist/browser-shell.d.ts +1 -1
  24. package/dist/browser-shell.js +2 -2
  25. package/dist/browser-workspace.d.ts +50 -2
  26. package/dist/browser-workspace.js +3 -3
  27. package/dist/build-review.js +2 -0
  28. package/dist/builder-restart.d.ts +21 -0
  29. package/dist/builder-restart.js +50 -0
  30. package/dist/builder.d.ts +11 -2
  31. package/dist/builder.js +234 -101
  32. package/dist/card-catalog.d.ts +209 -0
  33. package/dist/card-catalog.js +133 -0
  34. package/dist/card-cli.d.ts +43 -0
  35. package/dist/card-cli.js +44 -0
  36. package/dist/card-render.d.ts +26 -0
  37. package/dist/card-render.js +223 -0
  38. package/dist/card-sources.d.ts +68 -0
  39. package/dist/card-sources.js +234 -0
  40. package/dist/chat-acceptance.js +1 -1
  41. package/dist/chat-actions.d.ts +106 -9
  42. package/dist/chat-actions.js +473 -46
  43. package/dist/chat-approval.d.ts +13 -0
  44. package/dist/chat-approval.js +39 -9
  45. package/dist/chat-card-part.d.ts +11 -0
  46. package/dist/chat-card-part.js +18 -0
  47. package/dist/chat-card.d.ts +46 -0
  48. package/dist/chat-card.js +99 -0
  49. package/dist/chat-channel.d.ts +2 -0
  50. package/dist/chat-channel.js +37 -14
  51. package/dist/chat-controls.d.ts +4 -0
  52. package/dist/chat-controls.js +4 -0
  53. package/dist/chat-decide.d.ts +10 -16
  54. package/dist/chat-decide.js +79 -62
  55. package/dist/chat-delivery.js +13 -4
  56. package/dist/chat-flow.d.ts +0 -27
  57. package/dist/chat-flow.js +31 -2
  58. package/dist/chat-quiet.js +4 -4
  59. package/dist/chat-task-actions.d.ts +3 -2
  60. package/dist/chat-task-actions.js +19 -5
  61. package/dist/chat-terms.d.ts +14 -2
  62. package/dist/chat-terms.js +35 -0
  63. package/dist/chat-voice.d.ts +1 -1
  64. package/dist/chat-voice.js +2 -2
  65. package/dist/check-facts.d.ts +3 -1
  66. package/dist/check-facts.js +57 -11
  67. package/dist/check-failure.d.ts +24 -3
  68. package/dist/check-failure.js +89 -13
  69. package/dist/check-levels-ui.d.ts +4 -0
  70. package/dist/check-levels-ui.js +10 -1
  71. package/dist/check-load.d.ts +85 -0
  72. package/dist/check-load.js +147 -0
  73. package/dist/check-only-standing.d.ts +36 -0
  74. package/dist/check-only-standing.js +39 -0
  75. package/dist/check-only.d.ts +15 -0
  76. package/dist/check-only.js +99 -0
  77. package/dist/check-repair.d.ts +47 -0
  78. package/dist/check-repair.js +240 -0
  79. package/dist/check-report.d.ts +2 -0
  80. package/dist/check-report.js +1 -1
  81. package/dist/check-stamp.d.ts +46 -0
  82. package/dist/check-stamp.js +93 -0
  83. package/dist/checkout-cleanup.d.ts +36 -5
  84. package/dist/checkout-cleanup.js +166 -14
  85. package/dist/claim.d.ts +6 -0
  86. package/dist/claim.js +13 -2
  87. package/dist/cli-help.d.ts +4 -1
  88. package/dist/cli-help.js +49 -30
  89. package/dist/cli.js +15 -1
  90. package/dist/code-extract.d.ts +44 -0
  91. package/dist/code-extract.js +88 -0
  92. package/dist/code-map-acts.d.ts +24 -0
  93. package/dist/code-map-acts.js +53 -0
  94. package/dist/code-map-admission.d.ts +52 -0
  95. package/dist/code-map-admission.js +295 -0
  96. package/dist/code-map-build.d.ts +63 -0
  97. package/dist/code-map-build.js +415 -0
  98. package/dist/code-map-cli.d.ts +153 -0
  99. package/dist/code-map-cli.js +103 -0
  100. package/dist/code-map-main.d.ts +1 -0
  101. package/dist/code-map-main.js +48 -0
  102. package/dist/code-map-read.d.ts +106 -0
  103. package/dist/code-map-read.js +171 -0
  104. package/dist/code-map-report.d.ts +37 -0
  105. package/dist/code-map-report.js +95 -0
  106. package/dist/code-map-shape.d.ts +172 -0
  107. package/dist/code-map-shape.js +327 -0
  108. package/dist/code-map-store.d.ts +136 -0
  109. package/dist/code-map-store.js +233 -0
  110. package/dist/coding-provider.d.ts +3 -1
  111. package/dist/coding-provider.js +12 -7
  112. package/dist/coding-update.js +3 -2
  113. package/dist/coding-workspace.d.ts +1 -1
  114. package/dist/coding-workspace.js +8 -8
  115. package/dist/console-chrome.js +2 -2
  116. package/dist/containment.d.ts +5 -0
  117. package/dist/containment.js +14 -1
  118. package/dist/contracts/chat-actions.d.ts +92 -0
  119. package/dist/contracts/chat-actions.js +20 -0
  120. package/dist/contracts/chat-card.d.ts +91 -0
  121. package/dist/contracts/chat-card.js +54 -0
  122. package/dist/contracts/chat-content.d.ts +191 -0
  123. package/dist/contracts/chat-content.js +4 -0
  124. package/dist/contracts/cli.d.ts +52 -0
  125. package/dist/contracts/cli.js +4 -0
  126. package/dist/contracts/console-api.d.ts +12 -3
  127. package/dist/contracts/console-api.js +12 -3
  128. package/dist/contracts/flow-propose.d.ts +8 -1
  129. package/dist/contracts/flow-propose.js +3 -1
  130. package/dist/contracts/flow-send.d.ts +126 -0
  131. package/dist/contracts/flow-send.js +3 -0
  132. package/dist/contracts/gateway-tools.d.ts +17 -0
  133. package/dist/contracts/gateway-tools.js +3 -1
  134. package/dist/contracts/lead-tools.d.ts +234 -0
  135. package/dist/contracts/lead-tools.js +25 -1
  136. package/dist/contracts/project-knowledge.d.ts +38 -5
  137. package/dist/contracts/project-knowledge.js +11 -5
  138. package/dist/contracts/project-memory.d.ts +3 -1
  139. package/dist/contracts/project-memory.js +3 -2
  140. package/dist/contracts/storage.d.ts +14 -0
  141. package/dist/contracts/storage.js +3 -1
  142. package/dist/contracts/store-json.d.ts +18 -0
  143. package/dist/contracts/youiyoux-request.d.ts +1 -1
  144. package/dist/contracts/youiyoux-request.js +1 -1
  145. package/dist/desktop-access.js +6 -2
  146. package/dist/discord-chat.js +14 -0
  147. package/dist/dispatch.d.ts +1 -1
  148. package/dist/dispatch.js +25 -16
  149. package/dist/dispose.d.ts +19 -0
  150. package/dist/dispose.js +55 -9
  151. package/dist/easy-start.js +2 -3
  152. package/dist/eval-oracle.js +4 -1
  153. package/dist/evals.js +2 -1
  154. package/dist/evidence-pair.d.ts +53 -0
  155. package/dist/evidence-pair.js +128 -0
  156. package/dist/exhaustion.d.ts +17 -4
  157. package/dist/exhaustion.js +44 -4
  158. package/dist/export.js +1 -1
  159. package/dist/failing-first.d.ts +31 -0
  160. package/dist/failing-first.js +44 -0
  161. package/dist/fallback-schema.d.ts +8 -0
  162. package/dist/fallback-schema.js +40 -0
  163. package/dist/fallback.d.ts +138 -0
  164. package/dist/fallback.js +605 -0
  165. package/dist/fast-start.d.ts +136 -0
  166. package/dist/fast-start.js +431 -0
  167. package/dist/fast-terms.d.ts +9 -0
  168. package/dist/fast-terms.js +23 -0
  169. package/dist/fenced-check-coverage.d.ts +62 -0
  170. package/dist/fenced-check-coverage.js +54 -0
  171. package/dist/findings.d.ts +224 -0
  172. package/dist/findings.js +449 -0
  173. package/dist/first-run.js +1 -1
  174. package/dist/flaky-ci.d.ts +31 -0
  175. package/dist/flaky-ci.js +149 -0
  176. package/dist/flaky-cli.d.ts +60 -0
  177. package/dist/flaky-cli.js +60 -0
  178. package/dist/flaky.d.ts +75 -9
  179. package/dist/flaky.js +255 -0
  180. package/dist/fleet-schema.d.ts +17 -0
  181. package/dist/fleet-schema.js +135 -0
  182. package/dist/flow-browser.js +2 -2
  183. package/dist/flow-card-task.js +3 -0
  184. package/dist/flow-code.d.ts +2 -2
  185. package/dist/flow-code.js +9 -5
  186. package/dist/flow-result-card.d.ts +22 -0
  187. package/dist/flow-result-card.js +55 -0
  188. package/dist/flow-send.js +12 -9
  189. package/dist/flow-steps.js +1 -1
  190. package/dist/flow-triggers.d.ts +1 -1
  191. package/dist/flow-triggers.js +2 -2
  192. package/dist/flow-unattended.d.ts +26 -0
  193. package/dist/flow-unattended.js +12 -3
  194. package/dist/flows-ui.js +10 -2
  195. package/dist/follow-through-brief.d.ts +12 -0
  196. package/dist/follow-through-brief.js +31 -0
  197. package/dist/follow-through-cli.d.ts +23 -0
  198. package/dist/follow-through-cli.js +153 -0
  199. package/dist/git-incident.d.ts +16 -0
  200. package/dist/git-incident.js +69 -6
  201. package/dist/github-import.d.ts +62 -0
  202. package/dist/github-import.js +146 -0
  203. package/dist/guides.js +15 -10
  204. package/dist/handover-note.d.ts +33 -0
  205. package/dist/handover-note.js +106 -0
  206. package/dist/invoke.d.ts +25 -2
  207. package/dist/invoke.js +157 -58
  208. package/dist/job-object-helper.ps1 +10 -3
  209. package/dist/keymap.d.ts +41 -0
  210. package/dist/keymap.js +51 -0
  211. package/dist/knowledge-cli.js +1 -1
  212. package/dist/knowledge-ui.d.ts +21 -4
  213. package/dist/knowledge-ui.js +75 -7
  214. package/dist/laptop-background.d.ts +16 -0
  215. package/dist/laptop-background.js +108 -2
  216. package/dist/lead-cli.js +26 -24
  217. package/dist/lead-context.js +1 -1
  218. package/dist/lead-contract.d.ts +1 -1
  219. package/dist/lead-contract.js +3 -2
  220. package/dist/lead-doors.d.ts +4 -0
  221. package/dist/lead-doors.js +2 -2
  222. package/dist/lead-progress.js +5 -0
  223. package/dist/lead-status.d.ts +2 -0
  224. package/dist/lead-status.js +6 -2
  225. package/dist/lead-tools.js +162 -34
  226. package/dist/lead.js +2 -2
  227. package/dist/lesson-notes.d.ts +81 -0
  228. package/dist/lesson-notes.js +336 -0
  229. package/dist/limits-ui.js +1 -1
  230. package/dist/local-owner.d.ts +1 -1
  231. package/dist/local-owner.js +16 -0
  232. package/dist/machine-check.d.ts +16 -0
  233. package/dist/machine-check.js +26 -0
  234. package/dist/machine-identity.d.ts +75 -0
  235. package/dist/machine-identity.js +170 -0
  236. package/dist/mcp-core.js +11 -1
  237. package/dist/memory-cli.d.ts +300 -1
  238. package/dist/memory-cli.js +62 -9
  239. package/dist/memory-pass.d.ts +16 -0
  240. package/dist/memory-pass.js +67 -3
  241. package/dist/memory-tidy.d.ts +181 -0
  242. package/dist/memory-tidy.js +523 -0
  243. package/dist/memory-use.d.ts +35 -0
  244. package/dist/memory-use.js +109 -0
  245. package/dist/merge-checklist.d.ts +55 -0
  246. package/dist/merge-checklist.js +182 -0
  247. package/dist/modes.d.ts +6 -6
  248. package/dist/modes.js +3 -1
  249. package/dist/needs-you.d.ts +31 -9
  250. package/dist/needs-you.js +110 -41
  251. package/dist/nightly-github.d.ts +2 -0
  252. package/dist/nightly-github.js +4 -4
  253. package/dist/operate-remote-arguments.js +7 -1
  254. package/dist/operate-remote.js +3 -1
  255. package/dist/operate.d.ts +6 -1
  256. package/dist/operate.js +565 -64
  257. package/dist/phase-routing.js +2 -2
  258. package/dist/plain-schema.d.ts +23 -0
  259. package/dist/plain-schema.js +94 -0
  260. package/dist/plain-words.d.ts +20 -0
  261. package/dist/plain-words.js +66 -0
  262. package/dist/plan-login.d.ts +14 -0
  263. package/dist/plan-login.js +58 -0
  264. package/dist/planner.d.ts +1 -1
  265. package/dist/planner.js +4 -2
  266. package/dist/process-liveness.d.ts +2 -0
  267. package/dist/process-liveness.js +61 -17
  268. package/dist/project-delete-ui.d.ts +19 -2
  269. package/dist/project-delete-ui.js +29 -1
  270. package/dist/project-delete.js +14 -1
  271. package/dist/project-knowledge.d.ts +18 -11
  272. package/dist/project-knowledge.js +39 -9
  273. package/dist/project-learning.d.ts +2 -1
  274. package/dist/project-learning.js +15 -45
  275. package/dist/project-memory.d.ts +5 -0
  276. package/dist/project-memory.js +22 -2
  277. package/dist/project-tools.d.ts +4 -4
  278. package/dist/project-tools.js +23 -7
  279. package/dist/proof.js +30 -5
  280. package/dist/protected-storage-look.d.ts +1 -0
  281. package/dist/protected-storage-look.js +13 -0
  282. package/dist/protected-storage.d.ts +70 -39
  283. package/dist/protected-storage.js +196 -147
  284. package/dist/provider-auth.js +1 -1
  285. package/dist/provider-capture.d.ts +34 -0
  286. package/dist/provider-capture.js +53 -0
  287. package/dist/publish.js +9 -1
  288. package/dist/pull-request-flow.js +5 -0
  289. package/dist/push.d.ts +18 -2
  290. package/dist/push.js +40 -16
  291. package/dist/release-adapters.d.ts +9 -0
  292. package/dist/release-adapters.js +76 -0
  293. package/dist/release-auto.js +13 -13
  294. package/dist/release-card.d.ts +1 -2
  295. package/dist/release-card.js +11 -0
  296. package/dist/release-carry.d.ts +7 -2
  297. package/dist/release-carry.js +49 -10
  298. package/dist/release-cli.d.ts +4 -0
  299. package/dist/release-cli.js +29 -22
  300. package/dist/release-coverage.js +1 -1
  301. package/dist/release-next.d.ts +48 -0
  302. package/dist/release-next.js +161 -0
  303. package/dist/release.d.ts +20 -1
  304. package/dist/release.js +85 -9
  305. package/dist/remote-destination.d.ts +36 -0
  306. package/dist/remote-destination.js +217 -0
  307. package/dist/remote-main.d.ts +48 -0
  308. package/dist/remote-main.js +84 -5
  309. package/dist/repository-context.d.ts +3 -2
  310. package/dist/repository-context.js +112 -70
  311. package/dist/restart-certification.d.ts +1 -0
  312. package/dist/restart-certification.js +8 -6
  313. package/dist/result-acts.d.ts +27 -19
  314. package/dist/result-acts.js +36 -25
  315. package/dist/result-follow-ups.d.ts +2 -2
  316. package/dist/result-follow-ups.js +18 -4
  317. package/dist/result-review.d.ts +1 -1
  318. package/dist/route-change-check.d.ts +4 -0
  319. package/dist/route-change-check.js +13 -0
  320. package/dist/route-change.d.ts +22 -0
  321. package/dist/route-change.js +89 -0
  322. package/dist/route-hooks.js +3 -2
  323. package/dist/route-results.d.ts +6 -1
  324. package/dist/route-results.js +14 -5
  325. package/dist/run-closeout.d.ts +1 -1
  326. package/dist/run-closeout.js +7 -5
  327. package/dist/runner-process.js +3 -2
  328. package/dist/runner.js +2 -1
  329. package/dist/scope.d.ts +28 -4
  330. package/dist/scope.js +92 -28
  331. package/dist/scout.d.ts +1 -1
  332. package/dist/scout.js +2 -1
  333. package/dist/serve.d.ts +1 -1
  334. package/dist/serve.js +26 -18
  335. package/dist/server/chat.js +16 -14
  336. package/dist/server/chrome.d.ts +1 -1
  337. package/dist/server/chrome.js +26 -35
  338. package/dist/server/console.css +73 -0
  339. package/dist/server/guards.js +3 -0
  340. package/dist/server/home.d.ts +6 -0
  341. package/dist/server/home.js +21 -13
  342. package/dist/server/http.js +1 -1
  343. package/dist/server/kit.js +1 -1
  344. package/dist/server/merge-ready-view.d.ts +10 -0
  345. package/dist/server/merge-ready-view.js +30 -0
  346. package/dist/server/pages-assets.d.ts +1 -0
  347. package/dist/server/pages-assets.js +8 -5
  348. package/dist/server/pages.js +103 -82
  349. package/dist/server/render-chat.js +16 -3
  350. package/dist/server/render-code-map.d.ts +30 -0
  351. package/dist/server/render-code-map.js +137 -0
  352. package/dist/server/render-flaky.d.ts +20 -0
  353. package/dist/server/render-flaky.js +59 -0
  354. package/dist/server/render-follow-through.d.ts +11 -0
  355. package/dist/server/render-follow-through.js +68 -0
  356. package/dist/server/render-history.js +1 -1
  357. package/dist/server/render-new-task.d.ts +26 -1
  358. package/dist/server/render-new-task.js +30 -5
  359. package/dist/server/render-pages.d.ts +1 -175
  360. package/dist/server/render-pages.js +38 -313
  361. package/dist/server/render-projects.d.ts +2 -1
  362. package/dist/server/render-projects.js +9 -7
  363. package/dist/server/render-release.d.ts +2 -0
  364. package/dist/server/render-release.js +3 -3
  365. package/dist/server/render-results.d.ts +2 -0
  366. package/dist/server/render-results.js +68 -68
  367. package/dist/server/render-tasks.d.ts +16 -2
  368. package/dist/server/render-tasks.js +177 -141
  369. package/dist/server/retired-routes.d.ts +17 -0
  370. package/dist/server/retired-routes.js +53 -0
  371. package/dist/server/route-table.d.ts +3 -0
  372. package/dist/server/route-table.js +48 -12
  373. package/dist/server/settings.js +166 -34
  374. package/dist/server/task-views.js +66 -26
  375. package/dist/server/tasks.d.ts +2 -6
  376. package/dist/server/tasks.js +250 -321
  377. package/dist/server/welcome.js +1 -1
  378. package/dist/settings-groups-ui.d.ts +1 -0
  379. package/dist/settings-groups-ui.js +4 -2
  380. package/dist/settings-kit.js +3 -2
  381. package/dist/settings-proofs.js +3 -1
  382. package/dist/slack-chat.js +14 -1
  383. package/dist/steer-mailbox.d.ts +8 -1
  384. package/dist/steer-mailbox.js +164 -6
  385. package/dist/stop-proof.js +6 -4
  386. package/dist/storage-sweep-process.js +1 -1
  387. package/dist/storage-sweep.d.ts +5 -3
  388. package/dist/storage-sweep.js +30 -9
  389. package/dist/storage-ui.js +28 -17
  390. package/dist/storage.d.ts +27 -2
  391. package/dist/storage.js +39 -2
  392. package/dist/store.d.ts +89 -7
  393. package/dist/store.js +386 -114
  394. package/dist/surface.js +67 -6
  395. package/dist/sweep-quarantine.d.ts +1 -0
  396. package/dist/sweep-quarantine.js +6 -4
  397. package/dist/tamper-scan.d.ts +4 -0
  398. package/dist/tamper-scan.js +90 -0
  399. package/dist/task-aliases.d.ts +5 -0
  400. package/dist/task-aliases.js +9 -0
  401. package/dist/task-control.d.ts +2 -1
  402. package/dist/task-control.js +4 -1
  403. package/dist/task-outcome-cli.d.ts +2 -0
  404. package/dist/task-outcome-cli.js +17 -17
  405. package/dist/task-sizing.d.ts +5 -0
  406. package/dist/task-sizing.js +2 -0
  407. package/dist/task-source.d.ts +7 -0
  408. package/dist/task-source.js +14 -0
  409. package/dist/task-status.d.ts +9 -0
  410. package/dist/task-status.js +45 -49
  411. package/dist/teardown-git.d.ts +18 -0
  412. package/dist/teardown-git.js +105 -21
  413. package/dist/telegram-lead.js +23 -6
  414. package/dist/telegram-progress.js +5 -3
  415. package/dist/telegram-status.js +6 -4
  416. package/dist/telegram.js +12 -1
  417. package/dist/test-infra.d.ts +8 -0
  418. package/dist/test-infra.js +18 -0
  419. package/dist/tidy.js +1 -1
  420. package/dist/toolroll-update.js +19 -5
  421. package/dist/tui-contract.d.ts +24 -0
  422. package/dist/tui-source.js +67 -8
  423. package/dist/tui.js +51 -51
  424. package/dist/vanished-retry.d.ts +30 -0
  425. package/dist/vanished-retry.js +174 -0
  426. package/dist/work-index.d.ts +2 -0
  427. package/dist/work-index.js +95 -66
  428. package/dist/work-summary.d.ts +1 -1
  429. package/dist/work-summary.js +7 -2
  430. package/dist/workspace-ui.d.ts +7 -3
  431. package/dist/workspace-ui.js +38 -23
  432. package/dist/worktree.d.ts +12 -3
  433. package/dist/worktree.js +91 -14
  434. package/package.json +2 -1
  435. package/dist/server/render-next.d.ts +0 -63
  436. package/dist/server/render-next.js +0 -109
@@ -4,15 +4,15 @@
4
4
  * and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
5
5
  * newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
6
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. The change requests one youiyoux pass filed come as one card that shows every word of each
7
+ * A person with the authority approves a plan or merges from their own linked chat by default (owner's decision,
8
+ * docs/plans/toolroll-cards.md); their own off or limits (chat-approval.ts) and a signed mode's chatApprove term still
9
+ * hold. A tap is theirs alone, and the card binds it to the exact terms. The change requests one youiyoux pass filed come as one card that shows every word of each
11
10
  * task's terms, with Approve all and each one's own Approve (the requests card, below).
12
11
  *
13
12
  * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
14
13
  * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
15
14
  * console uses. */
15
+ import { routeChangeOf } from "./route-change.js";
16
16
  import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, releaseTaskRef } from "./store.js";
17
17
  import { isAcpProfile } from "./acp-agents.js";
18
18
  import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
@@ -22,18 +22,19 @@ import { chatControlHref, chatResultHref } from "./chat-controls.js";
22
22
  import { applyChatTaskAction, chatTaskStamp } from "./chat-task-actions.js";
23
23
  import { chatTitle } from "./chat-voice.js";
24
24
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
25
- import { effectiveChatApproval } from "./chat-approval.js";
26
- import { chatApproveChatsOf, chatNames, modeTermsFromJson } from "./modes.js";
25
+ import { chatApproveMode } from "./chat-approval.js";
27
26
  import { parseExecutionPlanDocument } from "./plan.js";
28
27
  import { verifyApproverStanding } from "./principal.js";
29
28
  import { pullRequestFollowOf } from "./pull-request-flow.js";
30
29
  import { requestResultChanges } from "./result-actions.js";
31
30
  import { revisionSourceOf } from "./result-review.js";
32
- import { APPROVAL_PROOF, approvalOf, approvalProofDetail, deployGateScope, quarantinedAuthor } from "./scope.js";
31
+ import { APPROVAL_PROOF, approvalOf, approvalProofDetail, approve, deployGateScope, quarantinedAuthor } from "./scope.js";
32
+ import { gateWords } from "./approval-policy.js";
33
33
  import { releaseCardOf } from "./release-card.js";
34
34
  import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
35
35
  import { phoneText, projectLabel } from "./telegram-status.js";
36
- import { permissionPlainWords, scopeTermsText, shownTermsDigest } from "./chat-terms.js";
36
+ import { cardTermsDigest, chatLimitProblem, permissionPlainWords, scopeTermsText, shownTermsDigest } from "./chat-terms.js";
37
+ import { chainTermsLines } from "./scope.js";
37
38
  import { YOUIYOUX_CARD_KIND, YOUIYOUX_FILED_VIA, YOUIYOUX_HOME_PATH } from "./contracts/youiyoux-request.js";
38
39
  import { FILED_PER_PASS, requestKindWord, requestPassOf } from "./youiyoux-intake.js";
39
40
  import { discordPlain } from "./reply-shape.js";
@@ -41,13 +42,18 @@ import { PLATFORM_LIMITS } from "./text-limits.js";
41
42
  export { permissionPlainWords };
42
43
  /** The words, said once. */
43
44
  export const DECIDE_LABELS = {
44
- accept: "Accept and finish",
45
- changes: "Request changes",
45
+ accept: "Accept",
46
+ changes: "Send back",
46
47
  retry: "Retry",
47
48
  approve: "Approve & start",
48
49
  "not-now": "Not now",
49
50
  merge: "Merge",
50
51
  look: "Look first",
52
+ /** A result's one link (docs/plans/plain-toolroll.md P1.5). */
53
+ open: "Open result",
54
+ /** A plan card's one button: it shows the full terms, and the Approve sits under them. Never an approval itself. */
55
+ review: "Review plan",
56
+ details: "Details",
51
57
  edit: "Edit",
52
58
  yes: "Yes",
53
59
  cancel: "Cancel",
@@ -61,33 +67,7 @@ const sha = (text) => createHash("sha256").update(text).digest("hex");
61
67
  const isAct = (one) => "act" in one;
62
68
  const title = (store, taskId) => phoneText(chatTitle(store, taskId), 60);
63
69
  // ---- the chatApprove term ----------------------------------------------------------
64
- /** What lets this person's chat on `channel` approve on this repository now: their lasting chat-approval setting
65
- * (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
66
- * app (a mode signed before the term named its apps covers Telegram alone). `digest` names that authority for the
67
- * ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
68
- export function chatApproveMode(store, repo, approver, now, channel) {
69
- if (repo === null)
70
- return { ok: false, why: "Approving from chat isn't turned on for this project." };
71
- const setting = effectiveChatApproval(store, repo, approver);
72
- if (setting.ok)
73
- return { ok: true, digest: setting.setting.digest, limits: setting.limits, source: "setting" };
74
- const mode = store.activeMode(repo, now);
75
- const terms = mode === null ? null : modeTermsFromJson(mode.termsJson);
76
- if (mode === null || terms === null || !terms.chatApprove)
77
- return setting;
78
- if (mode.signedBy !== approver || !store.accountCanAccess(approver, repo))
79
- return { ok: false, why: "Only the person who signed this project's mode can approve from chat." };
80
- const chats = chatApproveChatsOf(terms);
81
- if (!chats.includes(channel))
82
- return { ok: false, why: `Your mode approves from ${chatNames(chats)} only. Sign it again to approve from ${chatNames([channel])}.` };
83
- return { ok: true, digest: mode.digest, limits: { fullAccess: terms.permissionDefault === "escalated", capMicrousd: terms.perAttemptBudgetMicrousd }, source: "mode" };
84
- }
85
- function fullAccess(profile) {
86
- if (profile === null || profile === undefined)
87
- return false;
88
- return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
89
- : profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
90
- }
70
+ export { chatApproveMode };
91
71
  const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
92
72
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
93
73
  * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
@@ -115,19 +95,16 @@ from) {
115
95
  const family = store.taskFamilyOf(taskId, [ref.repo], false);
116
96
  if (family === null || family.problem !== null || family.current.id !== taskId)
117
97
  return { ok: false, why: "A newer version of this task is current." };
118
- // What the mode's seal refuses anyway, said first: model- or coordinator-written terms, an unreadable route.
119
- if (ref.coordinatorCid !== null || quarantinedAuthor(scope.proposedVia ?? null))
120
- return { ok: false, why: "This plan was written for you, so you approve it in Toolroll." };
98
+ // Terms a machine wrote, or checks that reach past the sandbox: a person reads every word before a yes, so the card shows them whole.
99
+ const whole = ref.coordinatorCid !== null || quarantinedAuthor(scope.proposedVia ?? null);
121
100
  if (scope.profileState === "unresolved" || scope.routeEra == null || (scope.termsProblem ?? null) !== null)
122
101
  return { ok: false, why: "This plan can't say exactly which agents run, so you approve it in Toolroll." };
123
- const yours = mode.source === "setting" ? "your chat approval setting" : "your mode";
124
- if (fullAccess(scope.profile) && !mode.limits.fullAccess)
125
- return { ok: false, why: `This plan asks for more access than ${yours} allows.` };
126
- const cap = mode.limits.capMicrousd;
127
- if (cap !== null && (scope.budgetMicrousd === null || scope.budgetMicrousd > cap))
128
- return { ok: false, why: `This plan has ${scope.budgetMicrousd === null ? "no attempt limit" : `a ${money(scope.budgetMicrousd)} attempt limit`}, more than ${yours === "your mode" ? "your mode's" : "your chat approval limit of"} ${money(cap)}.` };
129
- if (store.approvalGate(taskId, approver, "mode").verdict !== "seal")
130
- return { ok: false, why: "This plan touches protected work, so two people approve it in Toolroll." };
102
+ const limited = chatLimitProblem(scope, mode.limits, mode.source === "setting" ? "your chat approval setting" : "your mode");
103
+ if (limited !== null)
104
+ return { ok: false, why: limited };
105
+ const gate = store.approvalGate(taskId, approver, "person");
106
+ if (gate.verdict === "refuse")
107
+ return { ok: false, why: gateWords(gate) };
131
108
  if (store.scopePolicyRefusal(taskId) !== null)
132
109
  return { ok: false, why: "Your organisation's policy needs this plan approved in Toolroll." };
133
110
  // A revision approves only against notes that still verify, as on the console.
@@ -156,7 +133,8 @@ from) {
156
133
  if (!verified)
157
134
  return { ok: false, why: "The saved plan can't be verified here, so you approve it in Toolroll." };
158
135
  }
159
- return { ok: true, modeDigest: mode.digest, source: mode.source, scope, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}`) };
136
+ // Terms shown whole are part of what the card is bound to, so the digest kept on its buttons covers those words.
137
+ return { ok: true, modeDigest: mode.digest, source: mode.source, scope, whole, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}${whole ? `:${cardTermsDigest(scopeTermsText(scope))}` : ""}`) };
160
138
  }
161
139
  /** A ready pull request this person's chat may merge now, bound to the exact commit. */
162
140
  export function mergeInChat(store, taskId, run, approver, now, channel) {
@@ -390,9 +368,14 @@ export function planCardText(store, taskId, scope, root, channel) {
390
368
  const goal = scope.goal.split("\n").filter(one => one.trim() !== "").slice(0, 2).map(one => phoneText(one, 160));
391
369
  const allowing = [permissionPlainWords(scope.profile), scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`]
392
370
  .filter((one) => one !== null);
371
+ const changed = routeChangeOf(store, taskId);
372
+ // Fallback agents are terms: in full on every card, near the top, and never cut.
373
+ const chain = chainTermsLines(scope);
393
374
  if (channel === "telegram") {
394
375
  return [
395
376
  `Plan ready: ${title(store, taskId)}`,
377
+ ...(changed === null ? [] : [changed]),
378
+ ...chain,
396
379
  `Starting allows: ${allowing.join(" · ")}`,
397
380
  "",
398
381
  ...goal,
@@ -405,6 +388,8 @@ export function planCardText(store, taskId, scope, root, channel) {
405
388
  }
406
389
  return [
407
390
  `Plan ready: ${title(store, taskId)}`,
391
+ ...(changed === null ? [] : [changed]),
392
+ ...chain,
408
393
  "",
409
394
  ...goal,
410
395
  "",
@@ -433,7 +418,7 @@ export function decideOffer(store, target, who, now, channel, root) {
433
418
  const fresh = resultFresh(store, target.taskId, target.run, who, now, root);
434
419
  if (!fresh.ok)
435
420
  return null;
436
- const look = { label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") };
421
+ const look = { label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") };
437
422
  // A result accepted only with a reason ("Accept anyway") keeps its link: the reason is typed on the result.
438
423
  if (fresh.exception)
439
424
  return null;
@@ -461,7 +446,7 @@ export function decideOffer(store, target, who, now, channel, root) {
461
446
  catch {
462
447
  head = [title(store, target.taskId)];
463
448
  }
464
- return { text: [...head, "", needsPassword(plan.scope)].join("\n"), rows: [[{ label: "Approve in Toolroll", path: chatControlHref("approval", target.taskId) }]] };
449
+ return { text: [...head, "", needsFullCard(plan.scope)].join("\n"), rows: [[{ label: "Approve in Toolroll", path: chatControlHref("approval", target.taskId) }]] };
465
450
  }
466
451
  // A deploy gate is approved from chat only through its release card (chat-management.md §0a.5).
467
452
  if (release) {
@@ -473,15 +458,17 @@ export function decideOffer(store, target, who, now, channel, root) {
473
458
  rows: [[{ act: "approve", run: null, digest: releaseDigest(card), label: "Continue" }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: releaseDigest(card) }]],
474
459
  ...(channel === "telegram" ? { file: { name: card.fileName, text: card.full } } : {}) };
475
460
  }
476
- return { text: planCardText(store, target.taskId, plan.scope, root, channel),
477
- rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
461
+ // One line, one button and Details (docs/plans/plain-toolroll.md P1.5): Review plan shows the full terms in the
462
+ // card, and only there does Approve appear, bound to the same digest.
463
+ return { text: planLineOf(store, target.taskId, ref.id),
464
+ rows: [[{ act: "approve", run: null, digest: plan.digest, label: DECIDE_LABELS.review }], [{ label: DECIDE_LABELS.details, path: chatControlHref("approval", target.taskId) }]] };
478
465
  }
479
466
  case "requests": return requestsOffer(store, target.taskId, who, now, channel, root);
480
467
  case "merge": {
481
468
  const merge = mergeInChat(store, target.taskId, target.run, who.name, now, channel);
482
469
  if (!merge.ok)
483
470
  return null;
484
- return { text: null, rows: [[{ act: "merge", run: target.run, digest: merge.head }], [{ label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") }]] };
471
+ return { text: null, rows: [[{ act: "merge", run: target.run, digest: merge.head }], [{ label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") }]] };
485
472
  }
486
473
  }
487
474
  }
@@ -565,7 +552,7 @@ export function isDecideToken(store, token) {
565
552
  /** A release card's acts carry its card digest (release-card.ts), never a plan's. */
566
553
  const RELEASE_DIGEST = "release:";
567
554
  /** Why a password-only scope (passwordOnlySeal) isn't approved from chat, for a release or a plan. */
568
- const needsPassword = (scope) => `This ${deployGateScope(scope) ? "release" : "plan"}'s checks run outside the sandbox, so it's approved in Toolroll with your password.`;
555
+ const needsFullCard = (scope) => `This ${deployGateScope(scope) ? "release" : "plan"}'s checks run outside the sandbox, so this card can't approve it. Approve it in Toolroll, or ask your lead for its approval card, which shows every term.`;
569
556
  /** Whether only a person's password ceremony seals this scope (store.ts sealScopeApproval): checks that reach beyond the
570
557
  * sandbox (sign-ins, or none at all) or extra macOS services, sealed into its profile. */
571
558
  const passwordOnlySeal = (scope) => {
@@ -604,7 +591,7 @@ seat, root) {
604
591
  if (!deployGateScope(plan.scope))
605
592
  return { ok: false, why: "This plan isn't a release." };
606
593
  if (passwordOnlySeal(plan.scope))
607
- return { ok: false, why: needsPassword(plan.scope) };
594
+ return { ok: false, why: needsFullCard(plan.scope) };
608
595
  const ref = store.lookupRef(taskId);
609
596
  if (ref === null || nightlyTaskRef(store, ref.id))
610
597
  return { ok: false, why: NIGHTLY_CHECK_ONLY };
@@ -623,7 +610,7 @@ seat, root) {
623
610
  return { ok: true, card, modeDigest: plan.modeDigest, source: plan.source, scope: plan.scope };
624
611
  }
625
612
  const QUESTIONS = {
626
- accept: name => `Accept and finish "${name}"?`,
613
+ accept: name => `Accept "${name}"?`,
627
614
  retry: name => `Retry "${name}"? It queues another attempt.`,
628
615
  approve: name => `Approve and start "${name}"?`,
629
616
  merge: name => `Merge "${name}"?`,
@@ -698,7 +685,7 @@ function staleWhy(store, row, who, now, channel, root) {
698
685
  if (deployGateScope(plan.scope))
699
686
  return "This is a release: approve it from its release card.";
700
687
  if (passwordOnlySeal(plan.scope))
701
- return needsPassword(plan.scope);
688
+ return needsFullCard(plan.scope);
702
689
  return plan.digest === row.digest ? null : "The plan changed since this card was sent.";
703
690
  }
704
691
  case "merge": {
@@ -712,7 +699,7 @@ function staleWhy(store, row, who, now, channel, root) {
712
699
  /** The links a card keeps once its acts are spent or stale: where to look, never a token. */
713
700
  export function linksFor(target) {
714
701
  switch (target.kind) {
715
- case "result": return [[{ label: DECIDE_LABELS.look, link: { label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") } }]];
702
+ case "result": return [[{ label: DECIDE_LABELS.open, link: { label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") } }]];
716
703
  case "merge": return [[{ label: "Open task", link: { label: "Open task", path: chatControlHref("task", target.taskId) } }]];
717
704
  case "plan": return [[{ label: "Review & start", link: { label: "Review & start", path: chatControlHref("approval", target.taskId) } }]];
718
705
  case "failed": return [[{ label: "Open task", link: { label: "Open task", path: chatControlHref("task", target.taskId) } }]];
@@ -740,7 +727,7 @@ export function planWhereNow(store, taskId, who, now, root) {
740
727
  }
741
728
  return { said: `"${name}" is under way.`, rows: [] };
742
729
  }
743
- const QUESTION_STARTS = ["Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \"", "Approve release "];
730
+ const QUESTION_STARTS = ["Accept \"", "Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \"", "Approve release "];
744
731
  /** The card's words without a question this module appended. */
745
732
  function cardBody(shown) {
746
733
  const at = shown.lastIndexOf("\n\n");
@@ -847,6 +834,22 @@ export function applyDecideTap(store, seat, input) {
847
834
  return { ack: "Confirm below.", edit: { text: `${body}\n\n${asked}`,
848
835
  rows: [[{ label: DECIDE_LABELS.yes, token: yes }, { label: DECIDE_LABELS.cancel, token: cancel }]], tokens: [yes, cancel] } };
849
836
  }
837
+ // A plan card's Review plan: the full terms in the card, and under them the Approve (the yes, bound to the same
838
+ // digest the card was minted with, checked again at the tap). Approve is reachable only from here.
839
+ if (row.act === "approve") {
840
+ const plan = planInChat(store, row.taskId, who.name, now, seat.channel, input.root);
841
+ if (!plan.ok)
842
+ return stale(plan.why);
843
+ spend();
844
+ const spec = { act: "approve", run: null, digest: row.digest };
845
+ const yes = mint(store, seat, input.message, spec, row.taskId, "yes", now);
846
+ const later = mint(store, seat, input.message, { act: "not-now", run: null, digest: row.digest }, row.taskId, "offer", now);
847
+ // Terms a machine wrote go on the card word for word, or the yes isn't to what they say.
848
+ if (plan.whole && scopeTermsText(plan.scope).length > PLATFORM_LIMITS[seat.channel] - ROOM)
849
+ return stale(`This plan's ${TOO_LONG}, so you approve it in Toolroll.`);
850
+ return { ack: "Read the plan, then approve.", edit: { text: `${plan.whole ? `${name}\n\n${scopeTermsText(plan.scope)}` : planCardText(store, row.taskId, plan.scope, input.root, seat.channel)}\n\n${QUESTIONS.approve(name)} This approves exactly this plan.`,
851
+ rows: [[{ label: DECIDE_LABELS.approve, token: yes }], [{ label: DECIDE_LABELS.edit, link: { label: DECIDE_LABELS.edit, path: chatControlHref("approval", row.taskId) } }, { label: DECIDE_LABELS["not-now"], token: later }]], tokens: [yes, later] } };
852
+ }
850
853
  // The first tap asks; only the yes acts, bound to exactly what this button was.
851
854
  spend();
852
855
  const spec = { act: row.act, run: row.run, digest: row.digest };
@@ -905,9 +908,19 @@ export function applyDecideTap(store, seat, input) {
905
908
  if (deployGateScope(plan.scope))
906
909
  return notDone("This is a release: approve it from its release card.");
907
910
  if (passwordOnlySeal(plan.scope))
908
- return notDone(needsPassword(plan.scope));
909
- if (!sealPlanInChat(store, who, seat, ref.repo, row.taskId, plan, now))
911
+ return notDone(needsFullCard(plan.scope));
912
+ // The one approval every road shares (scope.ts approve): the rules, two-person votes and the proof, as this person.
913
+ // plan.digest matched this button's kept digest above (staleWhy), so these are the words its card showed.
914
+ const words = scopeTermsText(plan.scope);
915
+ if (plan.whole && (plan.digest !== row.digest || words.length > PLATFORM_LIMITS[seat.channel] - ROOM))
916
+ return notDone("this plan changed. Open it in Toolroll.");
917
+ const approved = approve(store, row.taskId, who.name, now, plan.scope.digest, "", {}, undefined, { modeDigest: plan.modeDigest, shown: plan.whole ? { expect: cardTermsDigest(words), terms: words } : null });
918
+ if (!approved.ok && approved.reason === "second-approver")
919
+ return done(gateWords({ verdict: "vote", have: approved.have, need: 2, already: approved.already }));
920
+ if (!approved.ok)
910
921
  return notDone("the plan couldn't be approved from chat. Open it in Toolroll.");
922
+ store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: "plan approved in chat", outcome: "approved",
923
+ source: "request", detail: `via ${seat.channel} · chat binding #${seat.binding} · ${plan.source} ${plan.modeDigest}` });
911
924
  return done("✓ Approved. Work starts when a worker is free.");
912
925
  }
913
926
  case "merge": {
@@ -1007,3 +1020,7 @@ export function decideTargetOf(row) {
1007
1020
  return { kind: "failed", taskId: row.taskId };
1008
1021
  return null;
1009
1022
  }
1023
+ /** A plan card's one line: the task's number, "plan ready", and its title (docs/plans/plain-toolroll.md P1.5). */
1024
+ function planLineOf(store, taskId, number) {
1025
+ return `#${number}: plan ready · ${title(store, taskId)}`;
1026
+ }
@@ -10,6 +10,9 @@ import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
10
10
  import { ChatState, ChatDeliveryError, chatHash, savedChatPart, } from "./chat-delivery-state.js";
11
11
  import { readChatActionBody, readChatMessageBody, readChatPairBody, readChatPart } from "./contracts/chat-content.js";
12
12
  import { readProposalActionRow } from "./contracts/chat-callback-rows.js";
13
+ import { cardContent, cardPart } from "./chat-card-part.js";
14
+ import { flowDoneCard } from "./flow-result-card.js";
15
+ import { cardsFirst, isCardProposal } from "./chat-card.js";
13
16
  import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisionParts, flowSendParts } from "./chat-flow.js";
14
17
  import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
15
18
  import { triggerConfigOf } from "./flow-triggers.js";
@@ -484,10 +487,9 @@ export async function processChatEvent(options) {
484
487
  task: image.taskId,
485
488
  run: image.run,
486
489
  });
487
- for (const proposal of store
488
- .listLeadProposals(turn.thread, ["pending"])
489
- .filter((p) => p.turn === turn.id))
490
- parts.push({ text: "", proposal: proposal.id });
490
+ // Cards first, so what a card leads to sits right under it. A card is an ordinary part: it confirms nothing.
491
+ for (const proposal of cardsFirst(store.listLeadProposals(turn.thread, ["pending"]).filter((p) => p.turn === turn.id)))
492
+ parts.push(isCardProposal(proposal) ? cardPart(proposal) : { text: "", proposal: proposal.id });
491
493
  state.plan(event.id, parts, nowOf(options));
492
494
  return true;
493
495
  }
@@ -655,6 +657,7 @@ export function applyChatAction(options, event, binding, repos) {
655
657
  // A paired chat seat speaks for its account's full authority (pairing is a full session's act).
656
658
  authority: "full",
657
659
  via: options.state.provider,
660
+ binding: binding.id,
658
661
  evidenceRoot: options.evidenceRoot,
659
662
  confirm: phase === "yes",
660
663
  deferSignal: (signal) => signals.push(signal),
@@ -1053,6 +1056,12 @@ function offerNotice(options, notification, binding, registry) {
1053
1056
  ts: "", thread: "", payload: {}, created: now.toISOString() });
1054
1057
  state.plan(id, flowSendParts(store, notification, state.provider), now);
1055
1058
  }
1059
+ else if (personal && flowDoneCard(store, notification, now) !== null) {
1060
+ // A card that reached Done: what it went through, as a card with its links.
1061
+ state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
1062
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
1063
+ state.plan(id, [cardContent(flowDoneCard(store, notification, now))], now);
1064
+ }
1056
1065
  else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
1057
1066
  // A subagent's question (v93): its options and "Answer in words" on the notice, for the person it asks.
1058
1067
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
@@ -1,30 +1,3 @@
1
- /**
2
- * Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
3
- * Send back that Telegram has (telegram-flow.ts), on the notice itself.
4
- *
5
- * - The notice carries the draft (split over as many parts as it needs, so
6
- * what is approved is what was read) and the buttons on its last part.
7
- * - Approve decides through the same door as the console (decideFlowCard),
8
- * as the paired person, for exactly the visit the notice was about, then
9
- * repaints the notice with what happened and no buttons.
10
- * - Edit and Send back open a prompt: the person's next message in their
11
- * chat with Toolroll is the new draft (which comes back with fresh
12
- * buttons) or the note. "cancel" leaves it, and a prompt lapses after
13
- * 30 minutes. Chat apps can't all name the message a reply answers, so
14
- * the prompt says plainly what the next message does.
15
- *
16
- * Every button is one opaque token for one visit of one card, on the part it
17
- * rides: a tap on any other message, from anyone but the paired person, or
18
- * for a card that moved on, changes nothing.
19
- *
20
- * A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
21
- * chooses" visit is the same with the zone's own options as buttons. Instead
22
- * of a tap, a reply in the notice's thread is the note for where replies go;
23
- * the first other message in their chat is asked about once ("Use this as
24
- * your note?"), and everything else goes to the lead as usual. Choices are
25
- * made through chooseFlowCard, by that person alone, for exactly that visit,
26
- * and spend that visit's buttons in every chat app.
27
- */
28
1
  import type { ChatProvider } from "./contracts/chat-tables.js";
29
2
  import { type ChatBinding, type ChatContent, type ChatEvent, type ChatState } from "./chat-delivery-state.js";
30
3
  import type { Store } from "./store.js";
package/dist/chat-flow.js CHANGED
@@ -1,3 +1,31 @@
1
+ /**
2
+ * Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
3
+ * Send back that Telegram has (telegram-flow.ts), on the notice itself.
4
+ *
5
+ * - The notice carries the draft (split over as many parts as it needs, so
6
+ * what is approved is what was read) and the buttons on its last part.
7
+ * - Approve decides through the same door as the console (decideFlowCard),
8
+ * as the paired person, for exactly the visit the notice was about, then
9
+ * repaints the notice with what happened and no buttons.
10
+ * - Edit and Send back open a prompt: the person's next message in their
11
+ * chat with Toolroll is the new draft (which comes back with fresh
12
+ * buttons) or the note. "cancel" leaves it, and a prompt lapses after
13
+ * 30 minutes. Chat apps can't all name the message a reply answers, so
14
+ * the prompt says plainly what the next message does.
15
+ *
16
+ * Every button is one opaque token for one visit of one card, on the part it
17
+ * rides: a tap on any other message, from anyone but the paired person, or
18
+ * for a card that moved on, changes nothing.
19
+ *
20
+ * A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
21
+ * chooses" visit is the same with the zone's own options as buttons. Instead
22
+ * of a tap, a reply in the notice's thread is the note for where replies go;
23
+ * the first other message in their chat is asked about once ("Use this as
24
+ * your note?"), and everything else goes to the lead as usual. Choices are
25
+ * made through chooseFlowCard, by that person alone, for exactly that visit,
26
+ * and spend that visit's buttons in every chat app.
27
+ */
28
+ import { cardContent } from "./chat-card-part.js";
1
29
  import { LIMITS } from "./decision.js";
2
30
  import { keptDraft } from "./flow-draft.js";
3
31
  import { decideFlowCard, flowCardHref, flowDefinitionOf } from "./flow-engine.js";
@@ -66,11 +94,12 @@ export function flowSendParts(store, notification, app) {
66
94
  const content = kept === null ? null : readFlowSend(kept.contentJson);
67
95
  if (content === null)
68
96
  return null;
97
+ const card = content.card === undefined ? [] : [cardContent(content.card)];
69
98
  const voiced = itemsText(content, notification.subject, app);
70
99
  if (voiced !== null)
71
- return voiced.map((text, index) => index < voiced.length - 1 ? { text, voice: true } : { text, voice: true, ...sendLinks(flowSendPaths(content)) });
100
+ return [...voiced.map((text, index) => index < voiced.length - 1 ? { text, voice: true } : { text, voice: true, ...sendLinks(flowSendPaths(content)) }), ...card];
72
101
  const all = pieces(chatFlowText(`${notification.subject}\n\n${notification.body}`));
73
- return all.map((piece, index) => index < all.length - 1 ? { text: piece } : { text: piece, ...sendLinks(flowSendPaths(content)) });
102
+ return [...all.map((piece, index) => index < all.length - 1 ? { text: piece } : { text: piece, ...sendLinks(flowSendPaths(content)) }), ...card];
74
103
  }
75
104
  /**
76
105
  * A "flow-decision" notice as chat parts, or null when it isn't one (or the
@@ -19,7 +19,7 @@ import { YOUIYOUX_CARD_KIND } from "./contracts/youiyoux-request.js";
19
19
  * (a sign-in or plan-limit pause, an evening digest). */
20
20
  const NEEDED_KINDS = new Set([
21
21
  "acceptance-ready", "acceptance-evidence", "flow-decision", "flow-card", "plan-ready", "report-ready",
22
- "stale-approval", "security-release", "secret-detected",
22
+ "stale-approval", "security-release", "secret-detected", "protected-storage",
23
23
  ]);
24
24
  /** What still wants a person who pairs after it landed, in every chat app: an open decision or an unresolved attention
25
25
  * notice. Routine history is never replayed to a chat that just arrived (store.ts WANTED_BEFORE_PAIRING, the same rule). */
@@ -102,7 +102,7 @@ export function quietCardView(store, taskRefs, now, root, viewer) {
102
102
  const heading = `${lines.length} tasks`;
103
103
  const projects = [...new Set(lines.map(one => projectLabel(one.project)))];
104
104
  const text = [heading, ...lines.map(one => `${one.icon} ${phoneText(one.title, 60)} · ${one.status}`), "", projects.join(", ")].join("\n");
105
- return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/work" } };
105
+ return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/tasks" } };
106
106
  }
107
107
  /** Facts that carry their own controls or must stand alone (a security alert, a decision, a flow card, a
108
108
  * screenshot, an acceptance packet, a youiyoux pass's card) are never folded into a batch. */
@@ -177,7 +177,7 @@ export function finishedView(store, batch, now, root, viewer) {
177
177
  return null;
178
178
  const text = batchLine(lines.map(one => one.fact));
179
179
  if (lines.length > 1)
180
- return { text, entities: [], link: { label: "Open", path: "/work" } };
180
+ return { text, entities: [], link: { label: "Open", path: "/tasks" } };
181
181
  const target = lines[0].target ?? null;
182
182
  return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
183
183
  }
@@ -244,7 +244,7 @@ export function enqueueEveningDigests(store, now, root) {
244
244
  if (body === null)
245
245
  return;
246
246
  if (store.enqueueNotification({ dedupeKey: `digest:evening:${person.account}:${day}`, kind: "evening-digest", subject: "Evening digest", body,
247
- link: "/work", source: { installation: true }, recipient: person.account }, now))
247
+ link: "/tasks", source: { installation: true }, recipient: person.account }, now))
248
248
  queued++;
249
249
  });
250
250
  }
@@ -7,8 +7,8 @@ export declare const CHAT_TASK_ACTIONS: {
7
7
  readonly detail: "Pauses this task once the run acknowledges the stop. Saved work remains; other tasks continue.";
8
8
  };
9
9
  readonly resume: {
10
- readonly label: "Review resume";
11
- readonly detail: "Opens password confirmation to lift this stop’s hold and continue from saved work. Other holds and approval requirements still apply.";
10
+ readonly label: "Resume task";
11
+ readonly detail: "Lifts this stop’s hold and continues from saved work. Other holds and approval requirements still apply.";
12
12
  };
13
13
  readonly retry: {
14
14
  readonly label: "Try again";
@@ -48,6 +48,7 @@ export declare function chatTaskRun(store: Store, task: string, operation: "stop
48
48
  export declare function applyChatTaskAction(store: Store, who: VerifiedApprover, payload: Record<string, unknown>, now: Date, web: boolean, controls?: Pick<StopRequest, "deferSignal"> & {
49
49
  via?: StopRequest["via"];
50
50
  evidenceRoot?: string;
51
+ person?: boolean;
51
52
  }): {
52
53
  ok: true;
53
54
  taskId: string;
@@ -1,12 +1,13 @@
1
1
  import { verifiedAuthor } from "./store.js";
2
- import { requestTaskStop, taskControlOf } from "./task-control.js";
2
+ import { chatApproveMode, effectiveChatApproval } from "./chat-approval.js";
3
+ import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
3
4
  import { authorizePlanUnderMode } from "./plan-auto.js";
4
5
  import { fileAddTestsTask, requestFollowUpChecks } from "./result-follow-ups.js";
5
6
  export const CHAT_TASK_ACTIONS = {
6
7
  // Build #1604 feedback: one short consequence that still says saved work
7
8
  // remains, other tasks continue, and the pause waits for acknowledgement.
8
9
  stop: { label: "Stop task", detail: "Pauses this task once the run acknowledges the stop. Saved work remains; other tasks continue." },
9
- resume: { label: "Review resume", detail: "Opens password confirmation to lift this stop’s hold and continue from saved work. Other holds and approval requirements still apply." },
10
+ resume: { label: "Resume task", detail: "Lifts this stop’s hold and continues from saved work. Other holds and approval requirements still apply." },
10
11
  retry: { label: "Try again", detail: "Queues another attempt. Existing approvals and holds still apply." },
11
12
  plan: { label: "Plan first", detail: "Requests a plan. Building still requires approval." },
12
13
  wait_for: { label: "Add dependency", detail: "Waits for the selected task to finish." },
@@ -52,9 +53,22 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
52
53
  const run = chatTaskRun(store, task, operation, now);
53
54
  if (run === null || run !== payload["run"])
54
55
  return { ok: false, message: "This attempt changed. Review the current task before confirming." };
55
- // Resume remains the existing nonce + password ceremony. A proposal only opens it.
56
- if (operation === "resume")
57
- return { ok: true, taskId: task, said: `Resume review requested for ${task}. Complete the password confirmation on the task to resume.` };
56
+ // Resuming is an approval: a person's own tap or signed-in click lifts the stop; any other road only asks for it.
57
+ if (operation === "resume") {
58
+ if (controls.person !== true)
59
+ return { ok: true, taskId: task, said: `Resume requested for ${task}. Finish it in your chat or on the task.` };
60
+ // Resuming is an approval: a person's own off holds here as on every other one.
61
+ const allowed = effectiveChatApproval(store, ref.repo, who.name);
62
+ if (!allowed.ok && controls.via !== "web") {
63
+ const mode = controls.via === undefined || controls.via === "cli" ? allowed : chatApproveMode(store, ref.repo, who.name, now, controls.via);
64
+ if (!mode.ok)
65
+ return { ok: false, message: mode.why };
66
+ }
67
+ if (!allowed.ok && controls.via === "web")
68
+ return { ok: false, message: allowed.why };
69
+ const resumed = resumeTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: controls.via ?? "web" }, now);
70
+ return resumed.ok ? { ok: true, taskId: task, said: `Stop released. ${resumed.gate ? "Other requirements still prevent the next attempt." : "The next attempt may start."}` } : { ok: false, message: resumed.detail };
71
+ }
58
72
  const { via, evidenceRoot: _root, ...rest } = controls;
59
73
  const result = requestTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: via ?? (web ? "web" : "cli"), ...rest }, now);
60
74
  return result.ok ? { ok: true, taskId: task, said: `Stop requested for ${task}, run #${run}.` }
@@ -1,9 +1,21 @@
1
- import type { Scope } from "./scope.js";
1
+ import { type Scope } from "./scope.js";
2
2
  /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
3
3
  export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
4
- export type ShownScope = Pick<Scope, "goal" | "outOfScope" | "touches" | "acceptance" | "profile" | "budgetMicrousd">;
4
+ export type ShownScope = Pick<Scope, "goal" | "outOfScope" | "touches" | "acceptance" | "profile" | "budgetMicrousd"> & Partial<Pick<Scope, "proposedChainJson">>;
5
5
  /** Every word of the terms a yes agrees to, as the card shows them: the goal, what's excluded, the files (or plainly any
6
6
  * file), each requirement with the evidence it needs, and what the agent may do and spend. Nothing is shortened. */
7
7
  export declare function scopeTermsText(scope: ShownScope): string;
8
8
  /** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
9
9
  export declare function shownTermsDigest(scope: ShownScope & Pick<Scope, "digest">): string;
10
+ /** The digest of the exact words a card showed, taken when the card was made and kept with it. */
11
+ export declare function cardTermsDigest(terms: string): string;
12
+ /** Whether these words show this scope as it is now: its whole terms text, word for word, and what only a person's own
13
+ * yes may agree to (checks past the sandbox, extra services). Part of the terms, or an earlier version's, is not. */
14
+ export declare function cardShowsScope(scope: ShownScope, terms: string): boolean;
15
+ /** Whether a scope lets its agent do anything, nothing asking first. */
16
+ export declare function scopeFullAccess(profile: Scope["profile"] | null | undefined): boolean;
17
+ /** Why a person's own limits refuse this scope, or null. */
18
+ export declare function chatLimitProblem(scope: Pick<Scope, "profile" | "budgetMicrousd">, limits: {
19
+ fullAccess: boolean;
20
+ capMicrousd: number | null;
21
+ }, yours: string): string | null;
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import { createHash } from "node:crypto";
8
8
  import { isAcpProfile } from "./acp-agents.js";
9
+ import { checkAccessWords } from "./check-access.js";
10
+ import { sandboxTermsWords } from "./project-sandbox.js";
11
+ import { chainTermsLines } from "./scope.js";
9
12
  /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
10
13
  export function permissionPlainWords(profile) {
11
14
  if (profile === null || profile === undefined)
@@ -38,9 +41,41 @@ export function scopeTermsText(scope) {
38
41
  ...(scope.acceptance.length === 0 ? ["You decide when you review the result."] : scope.acceptance.map(one => `• ${one.statement} (${one.evidence.join(", ")})`)),
39
42
  "",
40
43
  `Starting allows: ${allows.join(" · ")}`,
44
+ ...((chain) => chain.length === 0 ? [] : ["", "Fallback agents:", ...chain])(chainTermsLines(scope)),
41
45
  ].join("\n");
42
46
  }
43
47
  /** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
44
48
  export function shownTermsDigest(scope) {
45
49
  return createHash("sha256").update(JSON.stringify({ v: 1, scope: scope.digest, shown: scopeTermsText(scope) })).digest("hex");
46
50
  }
51
+ /** The digest of the exact words a card showed, taken when the card was made and kept with it. */
52
+ export function cardTermsDigest(terms) {
53
+ return createHash("sha256").update(JSON.stringify({ v: 2, shown: terms })).digest("hex");
54
+ }
55
+ /** Whether these words show this scope as it is now: its whole terms text, word for word, and what only a person's own
56
+ * yes may agree to (checks past the sandbox, extra services). Part of the terms, or an earlier version's, is not. */
57
+ export function cardShowsScope(scope, terms) {
58
+ if (!terms.includes(scopeTermsText(scope)))
59
+ return false;
60
+ const profile = scope.profile;
61
+ if (profile === null || profile === undefined)
62
+ return true;
63
+ const access = profile.checkAccess, mach = sandboxTermsWords(profile).mach;
64
+ return (access === undefined || terms.includes(checkAccessWords(access))) && (mach === null || terms.includes(mach));
65
+ }
66
+ /** Whether a scope lets its agent do anything, nothing asking first. */
67
+ export function scopeFullAccess(profile) {
68
+ if (profile === null || profile === undefined)
69
+ return false;
70
+ return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
71
+ : profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
72
+ }
73
+ /** Why a person's own limits refuse this scope, or null. */
74
+ export function chatLimitProblem(scope, limits, yours) {
75
+ if (scopeFullAccess(scope.profile) && !limits.fullAccess)
76
+ return `This plan asks for more access than ${yours} allows.`;
77
+ const cap = limits.capMicrousd;
78
+ if (cap !== null && (scope.budgetMicrousd === null || scope.budgetMicrousd > cap))
79
+ return `This plan has ${scope.budgetMicrousd === null ? "no attempt limit" : `a ${money(scope.budgetMicrousd)} attempt limit`}, more than ${yours === "your mode" ? "your mode's" : "your chat approval limit of"} ${money(cap)}.`;
80
+ return null;
81
+ }
@@ -39,7 +39,7 @@ export type FinishedFact = {
39
39
  /** The two buttons under a result ready for a person: the real next step, then a look first. */
40
40
  export declare const READY_ACTIONS: {
41
41
  readonly merge: "Merge";
42
- readonly complete: "Accept and finish";
42
+ readonly complete: "Accept";
43
43
  readonly look: "Look first";
44
44
  };
45
45
  /** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */