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
@@ -1,5 +1,7 @@
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 { endHoldout, setCodeMapSwitch } from "./code-map-acts.js";
4
+ import { codeMapOn } from "./code-map-store.js";
3
5
  import { dismissSuggestion, ruleById, stopTrial, suggestionsFor, suggestionWords, taskEvidenceWords, trialWords } from "./route-rules.js";
4
6
  import { CHAT_ACTION_LOCAL_ACTS, currentLocalOwner } from "./local-owner.js";
5
7
  import { fileTaskProposal } from "./proposal.js";
@@ -10,12 +12,24 @@ import { withActor } from "./actor.js";
10
12
  import { TEXT_LIMITS } from "./text-limits.js";
11
13
  import { publicChatText } from "./chat-display.js";
12
14
  import { gateWords } from "./approval-policy.js";
15
+ import { checkLeadIdentity } from "./lead-identity.js";
16
+ import { hasForbiddenControls } from "./decision.js";
17
+ import { activeUnattendedGrant, candidatesDigestOf, FLOW_ACCEPTANCE, parseUnattendedAsk, revokeUnattended, sealUnattended, unattendedCandidates, unattendedSignProblem, untilWords, workReposOf } from "./flow-unattended.js";
18
+ import { UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedHeadWords, unattendedLimitWords } from "./flow-unattended-words.js";
19
+ import { CHECK_ACTION, checkFindingAt, cleanFindingText, familyInReach, FINDING_EXCERPT_LIMIT, FINDING_ROUND_LIMIT, FINDING_TEXT_LIMIT, findingLine, findingStandingWords, getFinding, resendFinding, REVIEW_ACTION, reviewFindingAt, sendCheckFinding, sendFinding, sendReviewFinding } from "./findings.js";
20
+ import { lessonNoteOf } from "./lesson-notes.js";
21
+ import { decideProposal, listProposals } from "./memory-pass.js";
22
+ import { authPauseOf, liftAuthPause } from "./provider-auth.js";
23
+ import { isProviderId } from "./provider.js";
24
+ import { approverStandsNow, familyRepoOf } from "./tidy.js";
25
+ import { chatApproveMode, effectiveChatApproval } from "./chat-approval.js";
26
+ import { cardTermsDigest, chatLimitProblem, scopeTermsText } from "./chat-terms.js";
13
27
  import { createHash, randomBytes } from "node:crypto";
14
- import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
28
+ import { archiveTaskFamily, leadTurnKeepsProposals, restoreTaskFamily, verifiedAuthor } from "./store.js";
15
29
  import { isVerifiedApprover, reproveApprover, } from "./principal.js";
16
30
  import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
17
31
  import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
18
- import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
32
+ import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, machineActing, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
19
33
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
20
34
  import { readAuthModeStrict } from "./keys.js";
21
35
  import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
@@ -30,7 +44,7 @@ import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCar
30
44
  import { browserViewOf, liveBrowserGrant, revokeFlowBrowser, signFlowBrowser } from "./flow-browser.js";
31
45
  import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
32
46
  import { describeSchedule, parseSchedule } from "./flow-schedule.js";
33
- import { dirname } from "node:path";
47
+ import { basename, dirname } from "node:path";
34
48
  import { handleOf, parseSoul, SOUL_CHARS, SUBAGENT_TEMPLATES, subagentLabel } from "./subagents.js";
35
49
  import { createSubagentFrom, labelOf, nameOf, renamedSoul, saveSoul, setSubagentState } from "./subagent-admin.js";
36
50
  import { cleanMemory, editMemory, forgetMemory, tellSubagent } from "./subagent-memory.js";
@@ -94,7 +108,7 @@ export const CHAT_ACTIONS = {
94
108
  flow_starter: { label: "Switch on", protected: false, password: false },
95
109
  // Lead fan-out (docs/plans/lead-fan-out.md): files N tasks under one brief and approves each one's scope — the
96
110
  // person's ceremony (protected, with the password), N ordinary approvals of exactly the scopes shown.
97
- sweep_file: { label: "Start sweep", protected: true, password: true },
111
+ sweep_file: { label: "Start sweep", protected: true, password: false },
98
112
  // Stops every item of a sweep not yet ready; finished results stay. An ordinary act, like cancelling a task.
99
113
  sweep_cancel_rest: { label: "Cancel the rest", protected: false, password: false },
100
114
  flow_edit: { label: "Save flow", protected: false, password: false },
@@ -112,9 +126,9 @@ export const CHAT_ACTIONS = {
112
126
  flow_trigger_pause: { label: "Pause trigger", protected: false, password: false },
113
127
  flow_trigger_resume: { label: "Turn trigger on", protected: false, password: false },
114
128
  flow_trigger_remove: { label: "Remove trigger", protected: false, password: false },
115
- // Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their
116
- // own password on the secure screen, never on a chat card or a paired phone's tap. Turning it off lowers authority.
117
- flow_browser: { label: "Allow browser", protected: true, password: true },
129
+ // Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their own
130
+ // yes on a card that shows every term, in a chat they are signed in to or linked from. Turning it off lowers authority.
131
+ flow_browser: { label: "Allow browser", protected: true, password: false },
118
132
  flow_browser_off: { label: "Turn off browser", protected: false, password: false },
119
133
  subagent_create: { label: "Add subagent", protected: false, password: false },
120
134
  subagent_soul: { label: "Update soul file", protected: false, password: false },
@@ -133,22 +147,50 @@ export const CHAT_ACTIONS = {
133
147
  suggestion_not_now: { label: "Not now", protected: false, password: false },
134
148
  // Stopping a trial only lowers: new tasks build on the configured model; approved ones keep theirs.
135
149
  trial_stop: { label: "Stop trial", protected: false, password: false },
150
+ // The code map's switch and holdout (docs/plans/code-map.md §5): a person's yes on the card, re-proved in the write.
151
+ code_map_switch: { label: "Change code map", protected: false, password: false },
152
+ code_map_holdout_end: { label: "End holdout", protected: false, password: false },
153
+ // Chat and console parity (docs/plans/toolroll-cards.md): plain acts that lower authority or change no one's reach,
154
+ // each the console's own write. Ending a mode and turning a flow off its own only ever lower authority.
155
+ mode_revoke: { label: "End automatic approvals", protected: false, password: false },
156
+ flow_unattended_off: { label: "Stop running on its own", protected: false, password: false },
157
+ flow_archive: { label: "Archive flow", protected: false, password: false },
158
+ provider_resume: { label: "Resume agent", protected: false, password: false },
159
+ worker_note: { label: "Note on worker", protected: false, password: false },
160
+ lead_identity: { label: "Name your lead", protected: false, password: false },
161
+ task_archive: { label: "Put away task", protected: false, password: false },
162
+ task_restore: { label: "Restore task", protected: false, password: false },
163
+ incident_resolve: { label: "Resolve incident", protected: false, password: false },
164
+ memory_decide: { label: "Decide memory proposal", protected: false, password: false },
165
+ // Follow-through (findings.ts): not an approval. A person's yes to words shown on the card sends them to the agent.
166
+ finding_send: { label: "Send finding", protected: false, password: false },
167
+ finding_resend: { label: "Send finding back", protected: false, password: false },
168
+ // The rest of the approvals: a person's own yes on a card that shows every term.
169
+ task_reopen: { label: "Reopen task", protected: true, password: false },
170
+ revision_accept: { label: "Accept plan revision", protected: true, password: false },
171
+ revision_reject: { label: "Reject plan revision", protected: true, password: false },
172
+ flow_unattended: { label: "Let flow run on its own", protected: true, password: false },
136
173
  // What the owner's own lead knows about them: theirs alone, in no project (lead-about.ts).
137
174
  lead_about_you: { label: "Remember", protected: false, password: false },
138
- scope_approve: { label: "Approve work", protected: true, password: true },
139
- result_accept: { label: "Accept and finish", protected: true, password: false },
175
+ scope_approve: { label: "Approve work", protected: true, password: false },
176
+ result_accept: { label: "Accept", protected: true, password: false },
140
177
  task_cancel: { label: "Cancel task", protected: true, password: false },
141
- task_resume: { label: "Resume task", protected: true, password: true },
178
+ task_resume: { label: "Resume task", protected: true, password: false },
142
179
  };
143
180
  // Every action has a schema (contracts/chat-actions.ts) and every schema an action: a mismatch fails to compile.
144
181
  const SAME_ACTIONS = true;
145
182
  void SAME_ACTIONS;
146
- /** Protected actions a paired phone may confirm behind its own explicit
147
- * yes/cancel challenge instead of the console's secure screen. Password
148
- * actions and long or redacted terms never qualify. */
149
- export const CHALLENGE_ACTIONS = new Set(["result_accept"]);
183
+ /** Approvals (owner's decision, docs/plans/toolroll-cards.md): a person's own yes from their chat stands without a password.
184
+ * Only a person's tap in a linked chat, or their click in a signed-in browser, ever runs one. */
185
+ export const APPROVAL_ACTIONS = new Set(["scope_approve", "task_resume", "sweep_file", "flow_browser", "task_reopen", "revision_accept", "revision_reject", "flow_unattended"]);
186
+ /** Protected actions a linked chat may confirm behind its own explicit yes/cancel challenge instead of the review screen.
187
+ * Password actions and long or redacted terms never qualify: the card must show the exact terms. */
188
+ export const APPROVAL_TERM_CHARS = 3000;
189
+ export const CHALLENGE_ACTIONS = new Set(["result_accept", ...APPROVAL_ACTIONS]);
150
190
  /** Actions about the person themselves, in no project: their card carries no project (repo ""). */
151
- export const OWNER_ACTIONS = new Set(["lead_about_you"]);
191
+ export const OWNER_ACTIONS = new Set(["lead_about_you", "provider_resume", "worker_note", "lead_identity"]);
192
+ /** Actions the console refuses a project-limited account (route table: limited "deny"): the same here. */
193
+ const INSTANCE_ACTIONS = new Set(["mode_revoke", "flow_unattended_off", "flow_unattended", "provider_resume", "worker_note", "lead_identity", "incident_resolve", "memory_decide"]);
152
194
  export function isChatAction(value) {
153
195
  return typeof value === "string" && Object.hasOwn(CHAT_ACTIONS, value);
154
196
  }
@@ -262,6 +304,14 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
262
304
  const read = readChatActionRequest(operation, input);
263
305
  if (!read.ok)
264
306
  throw Error(`This action can't be read — ${read.issues.map(issue => issue.line).join("; ")}.`);
307
+ if (INSTANCE_ACTIONS.has(operation) && store.accountOf(who.name)?.projects !== null)
308
+ throw Error("That needs an account with access to every project, as in the console.");
309
+ if (operation === "provider_resume" || operation === "worker_note" || operation === "lead_identity") {
310
+ requireActor(store, who, "", operation);
311
+ const { request, terms, state, title } = instanceAction(store, who, operation, input);
312
+ const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
313
+ return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
314
+ }
265
315
  if (operation === "lead_about_you") {
266
316
  // The owner's own note: the card shows the line, and the line it replaces beside it.
267
317
  const checked = checkAboutYouLine(input["line"]);
@@ -284,7 +334,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
284
334
  const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
285
335
  return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
286
336
  }
287
- 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_") || operation.startsWith("suggestion_") || operation.startsWith("trial_")
337
+ const task = operation === "mode_revoke" || operation === "memory_decide" || operation === "incident_resolve" || operation === "finding_resend" || operation.startsWith("code_map_") || operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_") || operation.startsWith("suggestion_") || operation.startsWith("trial_")
288
338
  ? null
289
339
  : text(input, "task", 64);
290
340
  const flowTarget = operation.startsWith("flow_") && operation !== "flow_create" && operation !== "flow_script_save" && operation !== "flow_starter" ? flowTargetOf(store, input) : null;
@@ -297,13 +347,21 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
297
347
  if (operation === "subagent_answer" && (questionTarget === null || questionFlow === null))
298
348
  throw Error("That question isn't open any more.");
299
349
  const repo = operation === "sweep_cancel_rest" ? store.getSweep(integer(input, "sweep"))?.repo
300
- : flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
350
+ : operation === "incident_resolve" ? incidentRepoOf(store, integer(input, "incident"))
351
+ : operation === "finding_resend" ? getFinding(store, integer(input, "finding"))?.repo
352
+ : flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
301
353
  if (!repo)
302
354
  throw Error("Choose an available project.");
303
355
  requireActor(store, who, repo);
304
356
  const request = structuredClone(input), terms = [];
305
357
  let state, title = CHAT_ACTIONS[operation].label;
306
- if (operation.startsWith("skill_")) {
358
+ if (PARITY_ACTIONS.has(operation)) {
359
+ const made = parityAction(store, who, operation, input, repo, flowTarget?.flow ?? null, now, request, root);
360
+ state = made.state;
361
+ title = made.title;
362
+ terms.push(...made.terms);
363
+ }
364
+ else if (operation.startsWith("skill_")) {
307
365
  const view = skillsView(store, repo, who.name);
308
366
  state = {
309
367
  identity: view.identity,
@@ -620,7 +678,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
620
678
  const children = sweepChildScopes(store, who, repo, brief, cleaned.map(({ title, item }) => ({ title, item })), input["acceptance"], now);
621
679
  state = { children: children.map(one => one.fingerprint) };
622
680
  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(", ")}` : ""}`);
623
- 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.");
681
+ 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}`)),
682
+ // Each item's own terms, word for word: its yes is bound to this text.
683
+ ...children.map((one, index) => `Item ${index + 1} in full:\n${one.terms}`), "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.");
624
684
  }
625
685
  else if (operation.startsWith("flow_")) {
626
686
  const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
@@ -744,7 +804,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
744
804
  // The approval the person reads and the one their password signs: the digest is re-derived when they confirm.
745
805
  state = { flow: flow.id, revision: flow.revision, digest: view.digest };
746
806
  title = `Allow browser use in ${flow.name}`;
747
- terms.push(`Flow: ${flow.name} · project ${project}`, ...view.zones.map(zone => `${zone.title} runs ${zone.script} v${zone.version}\nScript digest ${zone.digest}`), "Each run of these zones starts a headless Chromium.", ...view.terms, `Approval digest ${view.digest}`);
807
+ terms.push(`Flow: ${flow.name} · project ${project}`,
808
+ // Each script by its short digest: the whole approval, every script's full digest included, is bound in the state.
809
+ ...view.zones.map(zone => `${zone.title} runs ${zone.script} v${zone.version}\nScript ${zone.digest.slice(0, 12)}`), "Each run of these zones starts a headless Chromium.", ...view.terms);
748
810
  }
749
811
  }
750
812
  else if (operation === "flow_trigger_pause" || operation === "flow_trigger_resume" || operation === "flow_trigger_remove") {
@@ -891,6 +953,17 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
891
953
  title = "Stop trying a cheaper model";
892
954
  terms.push(trialWords(store, trial), "New tasks build on the configured model again. Tasks a person already approved on the trial keep that model.");
893
955
  }
956
+ else if (operation === "code_map_switch") {
957
+ const on = input["on"] === true;
958
+ state = { on: codeMapOn(store, repo) };
959
+ title = on ? "Turn the code map on" : "Turn the code map off";
960
+ terms.push(on ? "Toolroll keeps a map of main for this project, and builds get it." : "No new maps are made, and builds get none. The last map stays saved.");
961
+ }
962
+ else if (operation === "code_map_holdout_end") {
963
+ state = { holdout: true };
964
+ title = "End the code map holdout";
965
+ terms.push("No new builds are enrolled. Builds already enrolled keep their arm, so the results stay comparable.");
966
+ }
894
967
  else if (operation.startsWith("decision_")) {
895
968
  // The staleness fence: the newest decision id and the active count, so a
896
969
  // card drafted before a subagent recorded or retired one is refused.
@@ -972,8 +1045,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
972
1045
  if (!authority.ok)
973
1046
  throw Error(authority.problem);
974
1047
  state["authMode"] = authority.authMode;
975
- terms.push(...describeScope(scope, undefined, taskEvidenceWords(store, scope.taskId, now))
976
- .filter((line) => !/^\s*(reference|approved)\s/.test(line))
1048
+ // The whole terms word for word (the yes is bound to this text: chat-terms.ts cardShowsScope), then everything else.
1049
+ terms.push(scopeTermsText(scope), ...describeScope(scope, undefined, taskEvidenceWords(store, scope.taskId, now))
1050
+ .filter((line) => !/^\s*(reference|approved|goal|not this|touches)\s/.test(line))
977
1051
  .map((line) => line
978
1052
  .trimStart()
979
1053
  .replace(/^why model\s+/, "Why this model: ")
@@ -1035,6 +1109,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
1035
1109
  terms.push(`Limitation: ${caveat}`);
1036
1110
  }
1037
1111
  }
1112
+ // An approval keeps the digest of the exact words its card shows, from now on: the yes is to these (scope.ts approve).
1113
+ if (APPROVAL_ACTIONS.has(operation))
1114
+ state["shown"] = cardTermsDigest(terms.join("\n"));
1038
1115
  const stamp = hash({
1039
1116
  operation,
1040
1117
  request,
@@ -1055,10 +1132,13 @@ export function sharedActionPayload(value) {
1055
1132
  /** A shortened or redacted preview is not complete consent. This rule lives
1056
1133
  * in the shared door, so a forged transport callback cannot bypass it. */
1057
1134
  function sharedActionContentNeedsReview(action) {
1135
+ // An approval's card has room for longer terms. Its words are compared exactly: anything the card would hide or
1136
+ // rewrite (a long hex value, a path, a secret) sends the person to the review page. Toolroll's own digest is bound in
1137
+ // the state, never in the terms, and a commit is written short, so an ordinary approval shows every word as stored.
1138
+ const approval = APPROVAL_ACTIONS.has(action.operation), cap = approval ? APPROVAL_TERM_CHARS : 1200;
1058
1139
  const content = [action.title, ...action.terms];
1059
- return (action.terms.join("\n").length > 1200 ||
1060
- content.some((value, index) => publicChatText(value, index === 0 ? 200 : 1200) !==
1061
- value.replace(/\s+/g, " ").trim()));
1140
+ return (action.terms.join("\n").length > cap ||
1141
+ content.some((value, index) => publicChatText(value, index === 0 ? 200 : cap) !== value.replace(/\s+/g, " ").trim()));
1062
1142
  }
1063
1143
  export function sharedActionNeedsReview(action) {
1064
1144
  return (CHAT_ACTIONS[action.operation].protected ||
@@ -1145,8 +1225,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1145
1225
  if ((options.authority === "local-owner" || currentLocalOwner() !== null) && (!isChatAction(payload.operation) || CHAT_ACTION_LOCAL_ACTS[payload.operation] === null))
1146
1226
  return refuse("refused", "Set a password to do this.");
1147
1227
  let live;
1228
+ let drafted;
1148
1229
  try {
1149
1230
  const saved = savedActionContext(store, who, id, "confirming");
1231
+ drafted = saved;
1150
1232
  if (hash(saved) !== hash(payload))
1151
1233
  return refuse("refused", "This action does not match the saved proposal.");
1152
1234
  live = prepareSharedAction(store, who, saved.operation, saved.request, options.root, now);
@@ -1160,31 +1242,24 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1160
1242
  }
1161
1243
  // The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
1162
1244
  const savedStamp = payload.stamp;
1163
- let personProved = null;
1164
1245
  payload = live;
1165
1246
  const config = CHAT_ACTIONS[live.operation], req = live.request;
1166
1247
  const challenged = options.via !== "web" &&
1167
1248
  options.via !== "cli" &&
1168
1249
  options.confirm === true &&
1169
1250
  sharedActionAllowsChallenge(payload);
1251
+ if (APPROVAL_ACTIONS.has(live.operation) && machineActing())
1252
+ return refuse("needs-confirm", "Only a person approves this.");
1253
+ // A lead's or remote token reaches the same web door: an approval needs a person's own session (or tap, above).
1254
+ if (APPROVAL_ACTIONS.has(live.operation) && options.via === "web" && options.authority !== "local-owner" && options.person !== true)
1255
+ return refuse("needs-confirm", "Approve this yourself, in your signed-in chat.");
1170
1256
  if (sharedActionNeedsReview(payload) && !challenged) {
1171
1257
  if (options.via !== "web" || !options.review || options.confirm !== true)
1172
1258
  return refuse("needs-confirm", "Review the complete action in the secure confirmation screen.");
1173
1259
  if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id, savedStamp, now))
1174
1260
  return refuse("needs-confirm", "This confirmation expired or was already used. Open the review again.");
1175
- // A scope approval proves its person inside approve() (one transaction with the seal, under the same freshness
1176
- // rule as the task page); here an empty field is only looked at, never spent (an explicit re-check is one-use).
1177
- const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
1178
- const proved = config.password && !emptyApproval ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
1179
- // How the person was proved, for an approval that records it (browser use: flow-browser.ts refuses any but a person's).
1180
- if (proved?.ok === true)
1181
- personProved = proved.method;
1182
- if (config.password &&
1183
- (emptyApproval
1184
- ? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
1185
- // Easy-start C5: a local owner's eligible approval takes no password (approve() proves it again in the seal).
1186
- && !(options.authority === "local-owner" && currentLocalOwner()?.act === "approve-plan" && typeof req["task"] === "string" && localApprovalEligible(store, who.name, req["task"]).ok)
1187
- : proved?.ok !== true))
1261
+ const proved = config.password ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
1262
+ if (config.password && proved?.ok !== true)
1188
1263
  return refuse("needs-confirm", "Confirm with your password in the secure screen.");
1189
1264
  }
1190
1265
  try {
@@ -1193,6 +1268,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1193
1268
  const act = () => store.savepoint(() => {
1194
1269
  const task = typeof req["task"] === "string" ? req["task"] : null, repo = payload.repo, actor = who.name;
1195
1270
  let taskId = task;
1271
+ if (LATE_APPROVALS.has(payload.operation))
1272
+ return runLateApproval(store, who, payload, repo => approvalBy(store, who, repo, now, options, drafted), approvalRoad(options), now);
1273
+ if (PARITY_ACTIONS.has(payload.operation) || OWNER_PARITY.has(payload.operation))
1274
+ return runParityAction(store, who, payload, now, options.root);
1196
1275
  if (payload.operation === "lead_about_you") {
1197
1276
  const next = withAboutYouLine(aboutYouOf(store, actor), String(req["line"]), Number(req["replaces"] ?? 0));
1198
1277
  if (!next.ok)
@@ -1229,7 +1308,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1229
1308
  // re-reads the flow, its zones and scripts in this transaction and refuses unless they are what the card showed.
1230
1309
  const flow = Number(payload.state["flow"]);
1231
1310
  // signFlowBrowser re-reads the account's generation and reach in its transaction (it may have narrowed since).
1232
- const signed = signFlowBrowser(store, { flow, signer: actor, proof: personProved, sawDigest: String(payload.state["digest"] ?? ""), console: { generation: who.generation } }, now);
1311
+ const by = approvalBy(store, who, repo, now, options, drafted);
1312
+ if (by.ok === false)
1313
+ throw Error(by.why);
1314
+ const signed = signFlowBrowser(store, { flow, signer: actor, proof: options.via === "web" ? "session" : "chat", sawDigest: String(payload.state["digest"] ?? ""), console: { generation: who.generation } }, now);
1233
1315
  if (!signed.ok)
1234
1316
  throw Error(signed.message);
1235
1317
  return { ok: true, taskId: null, said: "Browser allowed. Its browser zones can start Chromium now.", href: `/flows/${flow}` };
@@ -1249,7 +1331,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1249
1331
  return { ok: true, taskId: null, said, href: `/sweeps/${Number(req["sweep"])}` };
1250
1332
  }
1251
1333
  else if (payload.operation === "sweep_file") {
1252
- const done = fileSweep(store, who, id, payload, options.review?.password ?? "", now);
1334
+ const done = fileSweep(store, who, id, payload, approvalBy(store, who, payload.repo, now, options, drafted), now);
1253
1335
  return { ok: true, taskId: null, said: done.said, href: done.href };
1254
1336
  }
1255
1337
  else if (payload.operation === "kit_setup") {
@@ -1282,6 +1364,13 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1282
1364
  throw Error(done.problem);
1283
1365
  return { ok: true, taskId: null, said: "Trial stopped. New tasks build on the configured model.", href: `/settings/models?scope=${encodeURIComponent(repo)}#models-suggested` };
1284
1366
  }
1367
+ else if (payload.operation === "code_map_switch" || payload.operation === "code_map_holdout_end") {
1368
+ const door = currentLocalOwner() !== null ? { kind: "local-owner", actor } : { kind: "person", person: { actor: who.name, generation: who.generation, method: "session" } };
1369
+ const done = payload.operation === "code_map_switch" ? setCodeMapSwitch(store, door, repo, req["on"] === true, now) : endHoldout(store, door, repo, now);
1370
+ if (!done.ok)
1371
+ throw Error(done.problem);
1372
+ return { ok: true, taskId: null, said: payload.operation === "code_map_switch" ? `The code map is ${req["on"] === true ? "on" : "off"}.` : "The holdout has ended.", href: `/settings/project?repo=${encodeURIComponent(repo)}#code-map` };
1373
+ }
1285
1374
  else if (payload.operation === "suggestion_not_now") {
1286
1375
  // The rule door (route-store.ts proveRuleDoor) re-proves who says it: this computer's local owner only inside
1287
1376
  // its `dismiss-suggestion` act, or the confirming person.
@@ -1313,7 +1402,16 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1313
1402
  else if (payload.operation === "scope_approve") {
1314
1403
  const scope = store.getScope(task);
1315
1404
  // Cockpit B2: classified by the act, not the road — the same standard-approval freshness as /t/:id/approve.
1316
- const result = withFreshness("approve-standard", () => approve(store, task, actor, now, scope.digest, options.review.password));
1405
+ const by = approvalBy(store, who, repo, now, options, drafted);
1406
+ if (by.ok === false)
1407
+ throw Error(by.why);
1408
+ const limited = chatLimitProblem(scope, by.limits, "your chat approval setting");
1409
+ if (limited !== null)
1410
+ throw Error(limited);
1411
+ // A local owner keeps its own road (approve proves it again in the seal); everyone else, their own yes from chat,
1412
+ // to the scope and the words the card was made with.
1413
+ const sawDigest = String(drafted.state["scope"]?.digest ?? "");
1414
+ const result = withFreshness("approve-standard", () => approve(store, task, actor, now, sawDigest, options.review?.password ?? "", {}, undefined, by.chat));
1317
1415
  if (!result.ok) {
1318
1416
  // A first yes on protected work is kept, with how its person was proved: committed, never rolled back.
1319
1417
  // The card stays open (`awaiting`) for the second approver; the same person again changes nothing.
@@ -1340,11 +1438,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1340
1438
  throw Error(`Task was not cancelled: ${result.reason}.`);
1341
1439
  }
1342
1440
  else if (payload.operation === "task_resume") {
1441
+ const by = approvalBy(store, who, repo, now, options, drafted);
1442
+ if (by.ok === false)
1443
+ throw Error(by.why);
1343
1444
  const result = resumeTaskStop(store, {
1344
1445
  taskId: task,
1345
1446
  runId: Number(req["run"]),
1346
1447
  by: verifiedAuthor(actor),
1347
- via: "web",
1448
+ via: options.via,
1348
1449
  }, now);
1349
1450
  if (!result.ok)
1350
1451
  throw Error(result.detail);
@@ -1423,7 +1524,7 @@ function sweepChildScopeOf(store, taskId) {
1423
1524
  // The fingerprint covers the scope's every field, not only what is rendered (a profile setting no sentence names, such
1424
1525
  // as an agent's idle timeout, still makes the card stale): its exact terms with only identity left out.
1425
1526
  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;
1426
- return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words };
1527
+ return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words, terms: scopeTermsText(scope) };
1427
1528
  }
1428
1529
  /** Every child as filing would make it, found in a savepoint that is always rolled back: prepare never writes. */
1429
1530
  function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
@@ -1449,7 +1550,24 @@ function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
1449
1550
  });
1450
1551
  return found;
1451
1552
  }
1452
- function fileSweep(store, who, proposal, payload, password, now) {
1553
+ /** How this yes stands, and under whose limits: a person's own from chat, by their own chat setting, with the words and
1554
+ * digest the card was made with. A local owner's road has none: approve() proves it itself. */
1555
+ function approvalBy(store, who, repo, now, options, drafted) {
1556
+ if (options.authority === "local-owner")
1557
+ return { ok: true, limits: { fullAccess: true, capMicrousd: null } };
1558
+ if (options.via === "cli")
1559
+ return { ok: false, why: "Approve this in chat or in Toolroll." };
1560
+ const shown = { expect: String(drafted.state["shown"] ?? ""), terms: drafted.terms.join("\n") };
1561
+ if (options.via === "web") {
1562
+ const allowed = effectiveChatApproval(store, repo, who.name);
1563
+ return allowed.ok ? { ok: true, limits: allowed.limits, chat: { modeDigest: allowed.setting.digest, shown } } : { ok: false, why: allowed.why };
1564
+ }
1565
+ const allowed = chatApproveMode(store, repo, who.name, now, options.via);
1566
+ return allowed.ok ? { ok: true, limits: allowed.limits, chat: { modeDigest: allowed.digest, shown } } : allowed;
1567
+ }
1568
+ function fileSweep(store, who, proposal, payload, by, now) {
1569
+ if (by.ok === false)
1570
+ throw Error(by.why);
1453
1571
  const req = payload.request;
1454
1572
  const already = store.sweepOfProposal(proposal);
1455
1573
  if (already !== null)
@@ -1472,7 +1590,9 @@ function fileSweep(store, who, proposal, payload, password, now) {
1472
1590
  const waiting = [];
1473
1591
  for (const one of filed) {
1474
1592
  const scope = store.getScope(one.id);
1475
- const approved = scope === null ? null : withFreshness("approve-standard", () => approve(store, one.id, who.name, now, scope.digest, password));
1593
+ // A person's own limits hold for each item: one past them is filed and waits for its own approval.
1594
+ const approved = scope === null || chatLimitProblem(scope, by.limits, "your chat approval setting") !== null ? null
1595
+ : withFreshness("approve-standard", () => approve(store, one.id, who.name, now, scope.digest, "", {}, undefined, by.chat));
1476
1596
  if (approved === null || !approved.ok)
1477
1597
  waiting.push(one.title);
1478
1598
  }
@@ -1654,3 +1774,310 @@ function runFlowAction(store, payload, actor, repos, root, now) {
1654
1774
  throw Error(acted.message);
1655
1775
  return settle(card.flow, acted.said, card.id);
1656
1776
  }
1777
+ // ---- chat and console parity (docs/plans/toolroll-cards.md) ------------------------------------------------------------
1778
+ const LATE_APPROVALS = new Set(["task_reopen", "revision_accept", "revision_reject", "flow_unattended"]);
1779
+ const PARITY_ACTIONS = new Set(["mode_revoke", "flow_unattended_off", "flow_archive", "task_archive", "task_restore", "incident_resolve", "memory_decide", "finding_send", "finding_resend", ...LATE_APPROVALS]);
1780
+ const OWNER_PARITY = new Set(["provider_resume", "worker_note", "lead_identity"]);
1781
+ function incidentRepoOf(store, id) {
1782
+ const open = store.openIncidents().find(one => one.id === id);
1783
+ return open === undefined ? null : store.lookupRef(open.taskId)?.repo ?? null;
1784
+ }
1785
+ function instanceAction(store, who, operation, input) {
1786
+ if (operation === "provider_resume") {
1787
+ const provider = text(input, "provider", 40);
1788
+ if (!isProviderId(provider))
1789
+ throw Error("Choose an agent provider.");
1790
+ const pause = authPauseOf(store, provider);
1791
+ if (pause === null)
1792
+ throw Error("That agent isn't paused.");
1793
+ // The pause is the whole installation's: every project with work waiting on it is named, and must be in reach here.
1794
+ const repos = [...new Set(store.handle.prepare(`SELECT t.repo AS repo FROM run r JOIN task_ref t ON t.id = r.task_ref WHERE r.provider = ? AND r.reason = 'auth-expired' AND r.finished_at >= ?
1795
+ UNION SELECT repo FROM task_ref WHERE auth_wait_pause = ?`).all(provider, pause.openedAt, pause.id).flatMap(row => row["repo"] === null ? [] : [String(row["repo"])]))].sort();
1796
+ if (repos.some(one => !who.repos.includes(one)))
1797
+ throw Error("That agent has work waiting in a project outside your access here.");
1798
+ return { request: { provider }, title: `Resume ${provider}`, state: { pause: pause.id, repos }, terms: [`Lifts the sign-in pause on ${provider} and sends its waiting tasks back to the queue. Existing approvals and holds still apply.`, ...(repos.length === 0 ? [] : [`Waiting work in: ${repos.map(one => basename(one)).join(", ")}`])] };
1799
+ }
1800
+ if (operation === "worker_note") {
1801
+ const runner = text(input, "runner", 120), note = String(input["note"] ?? "").trim();
1802
+ if (note.length > 200 || hasForbiddenControls(note))
1803
+ throw Error("A worker's note is at most 200 plain characters.");
1804
+ const worker = store.listRunners().find(one => one.name === runner && one.retiredAt === null);
1805
+ if (worker === undefined)
1806
+ throw Error("No such worker.");
1807
+ return { request: { runner, note }, title: `Note on ${runner}`, state: { runner }, terms: [note === "" ? "Clears this worker's note." : `Sets this worker's note to: ${note}`] };
1808
+ }
1809
+ const checked = checkLeadIdentity(String(input["name"] ?? ""), String(input["persona"] ?? ""));
1810
+ if (!checked.ok)
1811
+ throw Error(checked.message);
1812
+ return { request: { name: checked.identity.name, persona: checked.identity.persona }, title: `Call your lead ${checked.identity.name}`, state: {}, terms: [`Name: ${checked.identity.name}`, `Persona: ${checked.identity.persona}`] };
1813
+ }
1814
+ const REOPEN_REFUSED = {
1815
+ "unknown-task": "This task is not external work.", "not-latched": "The tracker never closed this: there is nothing to reopen.",
1816
+ "not-seen-open": "The tracker hasn't been seen open again since the close. Reopen it there first; the next sync notices.",
1817
+ claimed: "This task is being built right now.", held: "A hold stands: lift it first.", "question-open": "An unanswered question stands: answer or close it first.",
1818
+ "incident-open": "An unresolved incident stands: resolve it first.", "bad-state": "This task is not in a state reopen can take.",
1819
+ };
1820
+ /** The one plan revision waiting on a decision for this task, by its id or as the only one. */
1821
+ function waitingRevision(store, task, asked) {
1822
+ const ref = store.lookupRef(task);
1823
+ const waiting = ref === null ? [] : store.listPlanRevisions(ref.id).filter(one => one.status === "blocked");
1824
+ const found = asked === undefined ? (waiting.length === 1 ? waiting[0] : null) : waiting.find(one => one.id === asked) ?? null;
1825
+ if (found === null)
1826
+ throw Error("That plan revision isn't waiting on a decision.");
1827
+ return found;
1828
+ }
1829
+ /** A finding follows its task's family, so every project the family reaches must be in reach (findings.ts, one gate). */
1830
+ function familyWithin(store, who, task) {
1831
+ if (familyInReach(store, who, task) === null)
1832
+ throw Error("This task's work reaches a project outside your access here.");
1833
+ }
1834
+ function parityAction(store, who, operation, input, repo, flow, now, request, root) {
1835
+ switch (operation) {
1836
+ case "finding_send": {
1837
+ const task = String(input["task"]), row = store.getTask(task);
1838
+ if (row === null)
1839
+ throw Error("No such task.");
1840
+ familyWithin(store, who, task);
1841
+ const given = ["summary", "pointer", "run"].filter(key => input[key] !== undefined);
1842
+ if (given.length !== 1)
1843
+ throw Error("Send one thing: words in summary, a review finding's pointer, or a failed check's run.");
1844
+ const head = `Sends this to the agent on ${row.title}. It reaches a running build now, or the next attempt's brief, as data to check. The agent must accept it or reject it with a reason; a repeat counts as another round.`;
1845
+ if (input["pointer"] !== undefined) {
1846
+ const read = reviewFindingAt(store, task, text(input, "pointer", 40));
1847
+ if (!read.ok)
1848
+ throw Error(read.message);
1849
+ const place = read.finding.file === null ? [] : [`Where: ${read.finding.file}${read.finding.line === null ? "" : `:${read.finding.line}`}`];
1850
+ return { state: { task, pointer: input["pointer"], finding: read.finding }, title: "Send a review finding", terms: [head, `Finding: ${cleanFindingText(read.finding.scenario, FINDING_TEXT_LIMIT)}`, ...place, `Severity: ${read.finding.severity}`, `Asks: ${REVIEW_ACTION}`] };
1851
+ }
1852
+ if (input["run"] !== undefined) {
1853
+ if (root === undefined)
1854
+ throw Error("The check's log can't be read here.");
1855
+ const read = checkFindingAt(store, root, task, integer(input, "run"));
1856
+ if (!read.ok)
1857
+ throw Error(read.message);
1858
+ return { state: { task, run: input["run"], excerpt: read.excerpt, head: read.head }, title: "Send a failed check", terms: [head, `Finding: ${read.summary}`, `Log: ${cleanFindingText(read.excerpt, FINDING_EXCERPT_LIMIT)}`, `Asks: ${CHECK_ACTION}`] };
1859
+ }
1860
+ const summary = cleanFindingText(text(input, "summary", FINDING_TEXT_LIMIT), FINDING_TEXT_LIMIT);
1861
+ if (summary === "")
1862
+ throw Error("Say what the finding is.");
1863
+ const file = input["file"] === undefined ? null : cleanFindingText(text(input, "file", 300), 300) || null;
1864
+ const line = file === null || input["line"] === undefined ? null : integer(input, "line");
1865
+ for (const key of ["file", "line"])
1866
+ delete request[key];
1867
+ request["summary"] = summary;
1868
+ if (file !== null)
1869
+ request["file"] = file;
1870
+ if (line !== null)
1871
+ request["line"] = line;
1872
+ return { state: { task, updatedAt: row.updatedAt }, title: "Send a finding", terms: [head, `Finding: ${summary}`, ...(file === null ? [] : [`Where: ${file}${line === null ? "" : `:${line}`}`]), `Asks: ${REVIEW_ACTION}`] };
1873
+ }
1874
+ case "finding_resend": {
1875
+ const finding = getFinding(store, integer(input, "finding"));
1876
+ if (finding === null)
1877
+ throw Error("No such finding.");
1878
+ familyWithin(store, who, finding.taskId);
1879
+ if (finding.state === "open")
1880
+ throw Error("That finding is already with the agent.");
1881
+ if (finding.state === "closed")
1882
+ throw Error("That finding is closed.");
1883
+ if (finding.state === "with-person")
1884
+ throw Error("That finding has used its rounds: it needs your decision on the task.");
1885
+ const next = finding.rounds >= FINDING_ROUND_LIMIT ? "Its rounds are used, so this hands it to a person on the task." : `This is round ${finding.rounds + 1} of ${FINDING_ROUND_LIMIT}.`;
1886
+ return { state: { finding: finding.id, state: finding.state, rounds: finding.rounds, answer: finding.answer?.detail ?? null }, title: `Send finding #${finding.id} back`, terms: [`Finding: ${findingLine(finding)}`, `How it stands: ${findingStandingWords(finding)}`, next] };
1887
+ }
1888
+ case "task_reopen": {
1889
+ const task = String(input["task"]), row = store.getTask(task);
1890
+ if (row === null)
1891
+ throw Error("No such task.");
1892
+ if (store.mirrorByTask(task) === null)
1893
+ throw Error(REOPEN_REFUSED["unknown-task"]);
1894
+ return { state: { task, updatedAt: row.updatedAt, taskState: row.state }, title: `Reopen ${row.title}`, terms: ["Takes this outside work back up: the tracker shows it open again, and Toolroll may build it. Existing approvals and holds still apply."] };
1895
+ }
1896
+ case "revision_accept":
1897
+ case "revision_reject": {
1898
+ const task = String(input["task"]), revision = waitingRevision(store, task, input["revision"]);
1899
+ request["revision"] = revision.id;
1900
+ const artifact = store.getArtifact(revision.artifact);
1901
+ if (artifact === null || root === undefined)
1902
+ throw Error("The revised plan can't be read here.");
1903
+ const read = readVerifiedArtifact(root, artifact);
1904
+ if (!read.ok || artifact.truncated || artifact.redacted)
1905
+ throw Error("The revised plan couldn't be verified in full.");
1906
+ return { state: { task, revision: revision.id, artifact: artifact.sha256, authority: revision.authorityDigest }, title: `${operation === "revision_accept" ? "Accept" : "Reject"} revision ${revision.revision} of this plan`,
1907
+ terms: [`Why the builder asked: ${revision.reason}`, `Changes: ${revision.changedFields.join(", ") || "the plan only"}${revision.authorityKind === "authority-change" ? ". This changes what was approved." : "."}`, `Plan:\n${read.content.toString("utf8")}`] };
1908
+ }
1909
+ case "flow_unattended": {
1910
+ const asked = parseUnattendedAsk({ days: input["days"], budget: input["budget"], runs: input["runs"], include: input["include"] });
1911
+ if (!asked.ok)
1912
+ throw Error(asked.message);
1913
+ const problem = unattendedSignProblem(store, flow, who.name);
1914
+ if (problem !== null)
1915
+ throw Error(problem);
1916
+ const candidates = unattendedCandidates(store, flow.id), include = asked.ask.include ?? [];
1917
+ const covered = candidates.filter(one => one.optional === null || include.includes(one.id));
1918
+ if (include.some(id => !candidates.some(one => one.id === id)))
1919
+ throw Error("Name schedules from this flow's own list.");
1920
+ if (covered.length === 0)
1921
+ throw Error("Choose a schedule to include: this flow's schedules bring in outside content, so none is covered unless you pick it.");
1922
+ // Every project its work files in: the person must reach each, and each is named and checked at the yes.
1923
+ const repos = [...new Set([flow.repo, ...workReposOf(flow)])];
1924
+ if (repos.some(one => !who.repos.includes(one) || !store.accountCanAccess(who.name, one)))
1925
+ throw Error("This flow files work in a project outside your access here.");
1926
+ return { state: { flow: flow.id, revision: flow.revision, schedules: candidatesDigestOf(candidates), ask: asked.ask, repos }, title: `Let ${flow.name} run on its own`,
1927
+ terms: [...unattendedHeadWords(who.name, FLOW_ACCEPTANCE), `Works in: ${repos.map(one => basename(one)).join(", ")}`, unattendedCoverWords(covered), ...covered.flatMap(one => one.optional === null ? [] : [`${one.words}: ${one.optional}`]), ...unattendedLimitWords(asked.ask.budgetMicrousd, asked.ask.dailyRunCap), ...UNATTENDED_RULE_WORDS, `It ends after ${asked.ask.days} ${asked.ask.days === 1 ? "day" : "days"}.`] };
1928
+ }
1929
+ case "mode_revoke": {
1930
+ const mode = store.activeMode(repo, now);
1931
+ if (mode === null)
1932
+ throw Error("No automatic approvals are on for this project.");
1933
+ return { state: { mode: mode.id, digest: mode.digest }, title: "End automatic approvals", terms: [`Ends the ${mode.name} mode signed by ${mode.signedBy}. Every act goes back to its own approval.`] };
1934
+ }
1935
+ case "flow_unattended_off": {
1936
+ const grant = activeUnattendedGrant(store, flow.id, now);
1937
+ if (grant === null)
1938
+ throw Error(`${flow.name} isn't running on its own.`);
1939
+ return { state: { flow: flow.id, grant: grant.id }, title: `Stop ${flow.name} running on its own`, terms: ["New cards wait for a person again."] };
1940
+ }
1941
+ case "flow_archive":
1942
+ return { state: { flow: flow.id, revision: flow.revision }, title: `Archive ${flow.name}`, terms: ["Takes this flow off the list. Its triggers stop starting cards."] };
1943
+ case "task_archive": {
1944
+ const task = String(input["task"]), row = store.getTask(task), reason = text(input, "reason", 500, true).trim();
1945
+ if (row === null)
1946
+ throw Error("No such task.");
1947
+ if (reason === "")
1948
+ delete request["reason"];
1949
+ else
1950
+ request["reason"] = reason;
1951
+ return { state: { task, updatedAt: row.updatedAt, state: row.state }, title: `Put away ${row.title}`, terms: ["Takes this task out of every list. It stays saved and can be restored.", ...(reason === "" ? [] : [`Reason: ${reason}`])] };
1952
+ }
1953
+ case "task_restore": {
1954
+ const task = String(input["task"]), row = store.getTask(task);
1955
+ if (row === null)
1956
+ throw Error("No such task.");
1957
+ return { state: { task, updatedAt: row.updatedAt }, title: `Restore ${row.title}`, terms: ["Puts this task back in the lists."] };
1958
+ }
1959
+ case "incident_resolve": {
1960
+ const id = integer(input, "incident"), open = store.openIncidents().find(one => one.id === id);
1961
+ return { state: { incident: id }, title: `Resolve the incident on ${open.taskId}`, terms: ["Marks this incident resolved. Nothing else about the task changes."] };
1962
+ }
1963
+ case "memory_decide": {
1964
+ const id = integer(input, "proposal"), decision = input["decision"] === "accept" ? "accept" : "reject";
1965
+ const proposal = listProposals(store, repo).find(one => one.id === id);
1966
+ if (proposal === undefined)
1967
+ throw Error("That proposal isn't waiting any more.");
1968
+ // A lesson note is accepted in the person's own words (memory-pass.ts): never a model's, so never from a card.
1969
+ if (decision === "accept" && lessonNoteOf(store, id) !== null)
1970
+ throw Error("A lesson note is accepted in your own words: write it on the Knowledge page.");
1971
+ return { state: { proposal: id }, title: `${decision === "accept" ? "Accept" : "Reject"} a memory proposal`, terms: [proposal.title, ...(proposal.beforeText === null ? [] : [`Was: ${proposal.beforeText}`]), `Becomes: ${proposal.afterText}`, `Why: ${proposal.rationale}`, decision === "accept" ? "Later agent sessions in this project read this." : "Nothing is added to project memory."] };
1972
+ }
1973
+ default:
1974
+ throw Error("Choose an available action.");
1975
+ }
1976
+ }
1977
+ /** Where a yes came from, in the words the other chat approvals' ledger lines use. */
1978
+ const approvalRoad = (options) => `via ${options.via}${options.binding === undefined ? "" : ` · chat binding #${options.binding}`}`;
1979
+ /** The approvals added last: the console's own write, after the same question every approval asks. */
1980
+ function runLateApproval(store, who, payload, byFor, road, now) {
1981
+ const req = payload.request, actor = who.name, by = byFor(payload.repo);
1982
+ // Rejecting lowers: it takes a person's yes, not their approving setting.
1983
+ if (by.ok === false && payload.operation !== "revision_reject")
1984
+ throw Error(by.why);
1985
+ if (payload.operation === "task_reopen") {
1986
+ const task = String(req["task"]), reopened = store.reopenMirror(task, actor, now);
1987
+ if (!reopened.ok)
1988
+ throw Error(REOPEN_REFUSED[reopened.reason] ?? "The task could not be reopened.");
1989
+ return { ok: true, taskId: task, said: "Reopened." };
1990
+ }
1991
+ if (payload.operation === "flow_unattended") {
1992
+ const ask = payload.state["ask"];
1993
+ // The person's own setting holds in every project this flow's work files in, not only its own.
1994
+ for (const repo of payload.state["repos"]) {
1995
+ const here = byFor(repo);
1996
+ if (here.ok === false)
1997
+ throw Error(here.why);
1998
+ if (here.limits.capMicrousd !== null && (ask.budgetMicrousd === null || ask.budgetMicrousd > here.limits.capMicrousd))
1999
+ throw Error("This has a higher attempt limit than your chat approval setting allows.");
2000
+ }
2001
+ const signed = sealUnattended(store, { flow: Number(req["flow"]), signer: actor, ask, sawRevision: Number(payload.state["revision"]), sawSchedules: String(payload.state["schedules"]), road }, now);
2002
+ if (!signed.ok)
2003
+ throw Error(signed.message);
2004
+ return { ok: true, taskId: null, said: `Runs on its own until ${untilWords(signed.grant.absoluteExpiry)}.`, href: `/flows/${Number(req["flow"])}` };
2005
+ }
2006
+ const task = String(req["task"]), revision = waitingRevision(store, task, req["revision"]), accept = payload.operation === "revision_accept";
2007
+ // A saved limit can't be read against a change of authority: that one opens in Toolroll.
2008
+ if (accept && by.ok && revision.authorityKind === "authority-change" && (!by.limits.fullAccess || by.limits.capMicrousd !== null))
2009
+ throw Error("This revision changes what was approved, beyond your chat approval limits. Open it in Toolroll.");
2010
+ if (!store.resolvePlanRevision(revision.id, accept ? "applied" : "rejected", actor, now))
2011
+ throw Error("That plan revision is no longer waiting on a decision.");
2012
+ store.releaseOwnedHold("revision", String(revision.id));
2013
+ return { ok: true, taskId: task, said: accept ? "Revision accepted." : "Revision rejected." };
2014
+ }
2015
+ /** Each runs the write the console's own handler calls, as this person, inside the confirm door's transaction. */
2016
+ function runParityAction(store, who, payload, now, root) {
2017
+ const req = payload.request, actor = who.name, repo = payload.repo;
2018
+ const done = (said, href, taskId = null) => ({ ok: true, taskId, said, ...(href === undefined ? {} : { href }) });
2019
+ switch (payload.operation) {
2020
+ case "finding_send":
2021
+ case "finding_resend": {
2022
+ const task = payload.operation === "finding_send" ? String(req["task"]) : getFinding(store, Number(req["finding"]))?.taskId ?? null;
2023
+ const sent = payload.operation === "finding_resend" ? resendFinding(store, Number(req["finding"]), now)
2024
+ : req["pointer"] !== undefined ? sendReviewFinding(store, task, String(req["pointer"]), actor, now)
2025
+ : req["run"] !== undefined ? sendCheckFinding(store, root ?? "", task, Number(req["run"]), actor, now)
2026
+ // The lead drafted these words and the person confirmed them: recorded as the lead's, sent by the person.
2027
+ : sendFinding(store, { taskId: task, source: "lead", by: actor, summary: String(req["summary"]), file: typeof req["file"] === "string" ? req["file"] : null, line: typeof req["line"] === "number" ? req["line"] : null }, now);
2028
+ if (!sent.ok)
2029
+ throw Error(sent.message);
2030
+ const said = sent.standing === "with-person" ? `Finding #${sent.id} needs a person's decision on the task.` : sent.standing === "already-sent" ? `Finding #${sent.id} was already sent.`
2031
+ : sent.standing === "running" ? `Finding #${sent.id} sent to the running build.` : `Finding #${sent.id} goes to the next attempt.`;
2032
+ return done(said, task === null ? undefined : `/t/${encodeURIComponent(task)}#findings`, task);
2033
+ }
2034
+ case "mode_revoke":
2035
+ if (!store.revokeMode(repo, actor, "operator", now))
2036
+ throw Error("That mode already ended.");
2037
+ return done("Automatic approvals are off. Every act goes back to its own approval.", "/mode");
2038
+ case "flow_unattended_off":
2039
+ if (!revokeUnattended(store, Number(req["flow"]), actor, now))
2040
+ throw Error("It wasn't running on its own.");
2041
+ return done("It no longer runs on its own. New cards wait for a person.", `/flows/${Number(req["flow"])}`);
2042
+ case "flow_archive":
2043
+ store.archiveFlow(Number(req["flow"]), actor, now);
2044
+ return done("Flow archived.", "/flows");
2045
+ case "task_archive":
2046
+ case "task_restore": {
2047
+ const task = String(req["task"]);
2048
+ const standing = (refs) => approverStandsNow(store, actor, familyRepoOf(store, refs));
2049
+ const made = payload.operation === "task_archive"
2050
+ ? archiveTaskFamily(store, task, { by: actor, byKind: "person", rule: null, reason: String(req["reason"] ?? "").trim() || "Put away from chat" }, now, { guard: standing })
2051
+ : restoreTaskFamily(store, task, actor, now, standing);
2052
+ if (!made.ok)
2053
+ throw Error(made.message);
2054
+ return done(payload.operation === "task_archive" ? "Put away." : "Restored.", undefined, task);
2055
+ }
2056
+ case "incident_resolve":
2057
+ if (!store.resolveIncident(Number(req["incident"]), actor, now))
2058
+ throw Error("That incident was already resolved.");
2059
+ return done("Incident resolved.");
2060
+ case "memory_decide": {
2061
+ const decision = req["decision"] === "accept" ? "accept" : "reject";
2062
+ decideProposal(store, { repo, actor, id: Number(req["proposal"]), decision }, now);
2063
+ return done(decision === "accept" ? "Added to project memory." : "Rejected.", `/settings/knowledge?repo=${encodeURIComponent(repo)}`);
2064
+ }
2065
+ case "provider_resume": {
2066
+ const lifted = liftAuthPause(store, String(req["provider"]), "person", actor, now);
2067
+ if (lifted === null)
2068
+ throw Error("That agent isn't paused any more.");
2069
+ return done(`Resumed. ${lifted.resumed} ${lifted.resumed === 1 ? "task went" : "tasks went"} back to the queue.`);
2070
+ }
2071
+ case "worker_note": {
2072
+ const note = String(req["note"]);
2073
+ if (!store.setRunnerQueueNote(String(req["runner"]), note === "" ? null : note).ok)
2074
+ throw Error("No such worker.");
2075
+ return done("Note saved.", "/fleet");
2076
+ }
2077
+ case "lead_identity":
2078
+ store.setLeadConfig(actor, String(req["name"]), String(req["persona"]), now);
2079
+ return done(`Your lead is called ${String(req["name"])} now.`, "/settings/lead");
2080
+ default:
2081
+ throw Error("Choose an available action.");
2082
+ }
2083
+ }