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
package/dist/store.js CHANGED
@@ -9,6 +9,10 @@ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
9
9
  import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
10
10
  import { reclaimDatabase } from "./database-reclaim.js";
11
11
  import { BASELINE_SCHEMA, BASELINE_SCHEMA_VERSION } from "./store-baseline.js";
12
+ import { codeMapRemoteChanged, codeMapShapeProblem, forgetCodeMap, V152_ADDED_COLUMNS, V152_SCHEMA } from "./code-map-store.js";
13
+ import { readStamps, stampDigest, stampRefusal, V154_SCHEMA } from "./check-stamp.js";
14
+ import { FINDING_SOURCES, FINDING_SOURCES_BEFORE_LEAD, V155_SCHEMA, V155_STEPS } from "./fleet-schema.js";
15
+ import { V156_SCHEMA, V156_STEPS } from "./fallback-schema.js";
12
16
  import { refArchived, taskArchived } from "./task-archive.js";
13
17
  export { refArchived, taskArchived } from "./task-archive.js";
14
18
  import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
@@ -73,16 +77,19 @@ import { ownedProcessCount, runOwnerTag } from "./exec.js";
73
77
  import { worktreeProcessOccupancy } from "./worktree.js";
74
78
  import { processMayBeAlive } from "./process-liveness.js";
75
79
  import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
80
+ import { currentMachineId, isThisMachine, liveHostAliases, rememberHostAliases, thisMachine } from "./machine-identity.js";
76
81
  import { containerEmptiness } from "./container-state.js";
77
82
  import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
83
+ import { ACTIVITY_ONLY_KINDS } from "./needs-you.js";
78
84
  import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
79
85
  import { foldReview, manualReviewOnly } from "./proof.js";
80
86
  import { hashPassword, liveReleaseOf } from "./scope.js";
81
87
  import { currentLocalOwner, LOCAL_MODE_CONFINES, LOCAL_OWNER_LAPSED, localModeOn, localOwnerInFlight, localOwnerStandsNow } from "./local-owner.js";
82
88
  import { nightlyDebtHold } from "./nightly-github.js";
83
89
  import { releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
84
- import { APPROVAL_BASES, approvalOf, machineActing, personProof, proofDetailOf, quarantinedAuthor, sealOriginOf, digestOf, canonicalProfileJson, chainFromJson, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull } from "./scope.js";
90
+ import { APPROVAL_BASES, approvalOf, machineActing, personProof, proofDetailOf, quarantinedAuthor, sealOriginOf, digestOf, canonicalProfileJson, chainFromJson, liveChainFromJson, CHAIN_DIGEST_VERSION, LIVE_CHAIN_VERSION, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull } from "./scope.js";
85
91
  import { resolveScopeProfile, resolveRouteCandidates, exactPinOf, routeOfTask, agentChoicesFor } from "./agentconfig.js";
92
+ import { bindRunToWalk, chainForFiling, chainJsonOf, chainSealProblem, fallbackStampProblem } from "./fallback.js";
86
93
  import { canonicalOverridesJson, canonicalRouteJson, isRiskLevel, isTaskSize, headroomFrom, makesNoPlan, legOf, overridesFromJson, phaseOfRole, recommendRoute, routeDigestOf, routeFromJson, routeProblems, routeStampProblem, ROUTE_ERA, PROVIDER_ID_LIST, } from "./phase-routing.js";
87
94
  import { readAuthModeStrict } from "./keys.js";
88
95
  import { modeDigestOf, modeTermsFromJson } from "./modes.js";
@@ -92,6 +99,10 @@ import { authenticate as runnerAuthenticate } from "./runner.js";
92
99
  import { stampNewProjectLevel } from "./check-levels.js";
93
100
  import { parseCheckProgressSnapshot } from "./check-progress.js";
94
101
  import { authorityStandsSql } from "./approval-authority.js";
102
+ import { clearRetryEpisode, settleVanished } from "./vanished-retry.js";
103
+ import { issueImported } from "./task-source.js";
104
+ import { BUILDER_SERVICE_SCHEMA, CHECK_ONLY_SCHEMA, FAST_PREVIEW_SCHEMA, ROUTE_CHANGE_SCHEMA, V153_ADDED_COLUMNS, VANISHED_SCHEMA } from "./plain-schema.js";
105
+ export { V153_ADDED_COLUMNS } from "./plain-schema.js";
95
106
  import { releasePolicyDigest, serializeReleaseCheckPurpose, storedReleaseCheckPurpose, storedReleaseTerms, readReleaseTerms, readReleasePolicy } from "./contracts/release-terms.js";
96
107
  import { accountReachesSql, normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
97
108
  import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
@@ -107,7 +118,7 @@ import { REQUEST_BUDGET_SCHEMA } from "./request-budget.js";
107
118
  import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
108
119
  import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, subagentFilers, usd } from "./spend.js";
109
120
  import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
110
- import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
121
+ import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, DEFAULT_IDLE, V151_ADDED_COLUMNS, cleanupWords, idleWords } from "./storage.js";
111
122
  import { inReadOnlyRequest } from "./read-only-request.js";
112
123
  import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
113
124
  import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
@@ -116,7 +127,7 @@ import { PULL_REQUEST_SCHEMA } from "./pull-request-schema.js";
116
127
  import { chatTurnAmountOf, combineReportedModels, providerTurnsOf, reportedCostUsdOf, reportedModelOf, runAmountOf } from "./reported-usage.js";
117
128
  import { usageOf } from "./run-efficiency.js";
118
129
  import { YOUIYOUX_FILED_VIA } from "./contracts/youiyoux-request.js";
119
- import { shownTermsDigest } from "./chat-terms.js";
130
+ import { cardShowsScope, cardTermsDigest, shownTermsDigest } from "./chat-terms.js";
120
131
  // PR #263 follow-up: the built-ins the Store money seal uses, captured as this module is evaluated, so code that later
121
132
  // replaces `Object.defineProperty` and the like can't fool the seal or the lock (lockStoreMoneyMethods refuses when
122
133
  // they've been replaced). The four sealed method references are recorded once, at the seal, in SEALED_MONEY_METHODS.
@@ -1117,7 +1128,21 @@ const REGISTERED_AT_FILING = "again, as it was filed";
1117
1128
  // v149 adds, and changes no saved row (docs/plans/learning-loops.md): every learning-loops table (V149_SCHEMA, new
1118
1129
  // tables only) — each check's recorded conditions and results, mixed-outcome observations, tracked identities, lesson
1119
1130
  // notes and their tallies, memory use, tidy-up proposals and decision merges.
1120
- export const SCHEMA_VERSION = 149;
1131
+ // v150 adds, and changes no saved row: this machine's stable id on each process witness (run_process.machine_id;
1132
+ // saved witnesses read NULL and keep being matched by host name) and the hostnames each machine id was seen under
1133
+ // (machine_host_alias, a new table; machine-identity.ts). V150_SCHEMA, V150_ADDED_COLUMNS.
1134
+ // v151 adds, and changes no saved row: how long a task stays idle before its clean folder goes
1135
+ // (V151_ADDED_COLUMNS: a saved cleanup setting reads NULL, the default).
1136
+ // v152 adds, and changes no saved row (docs/plans/code-map.md): the code map's tables (V152_SCHEMA, code-map-store.ts) and
1137
+ // a project's map switch and enrollment end (V152_ADDED_COLUMNS: a saved project reads on, not enrolled).
1138
+ // v153 adds, and changes no saved row (docs/plans/plain-toolroll.md Phase 2): new tables and columns only.
1139
+ // V153_SCHEMA, V153_ADDED_COLUMNS.
1140
+ // v154 adds, and changes no saved row (docs/plans/trusted-release-reports.md, H2): each check launch's execution stamp
1141
+ // (check_stamp, a new table; check-stamp.ts). A run from before reads no stamp: how its checks ran is unknown.
1142
+ // v155 adds, and changes no saved row (docs/plans/fleet-follow-through.md): findings and their answers, check repairs,
1143
+ // before-and-after evidence and provider captures (V155_SCHEMA, new tables only; fleet-schema.ts).
1144
+ // v156 adds fallback chains (V156_SCHEMA, new tables only; fallback-schema.ts), after v155.
1145
+ export const SCHEMA_VERSION = 156;
1121
1146
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
1122
1147
  export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
1123
1148
  /** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
@@ -1250,7 +1275,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
1250
1275
  * unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
1251
1276
  * (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
1252
1277
  */
1253
- export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149]);
1278
+ export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156]);
1254
1279
  /** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
1255
1280
  export const V119_ADDED_COLUMNS = Object.freeze({
1256
1281
  task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
@@ -1876,6 +1901,41 @@ export const V149_TABLES = Object.freeze(["check_result", "check_fact", "local_o
1876
1901
  export const V149_REPO_TABLES = Object.freeze(["check_fact", "local_observation", "ci_flaky_observation", "learning_watch", "lesson_note", "memory_use", "memory_tidy_proposal"]);
1877
1902
  /** The v149 migration's steps after v148's, in order (OpenOptions.migrationStep may stop after each). */
1878
1903
  export const V149_STEPS = Object.freeze(["learning-tables"]);
1904
+ /**
1905
+ * v150 (machine-identity.ts): which computer a process witness belongs to, by the machine's stable id rather than its
1906
+ * network hostname (which follows the network on a Mac with no HostName set). machine_host_alias records each
1907
+ * hostname this machine read while it knew its own id, written only by a process on that machine, beside the id it
1908
+ * read; a witness with no id (every saved one) is this machine's when its host is one of them. Never read for any
1909
+ * other machine's id.
1910
+ */
1911
+ export const V150_SCHEMA = `
1912
+ CREATE TABLE IF NOT EXISTS machine_host_alias (
1913
+ machine_id TEXT NOT NULL CHECK (length(machine_id) = 32),
1914
+ host TEXT NOT NULL CHECK (host <> ''),
1915
+ first_seen_at TEXT NOT NULL,
1916
+ PRIMARY KEY (machine_id, host)
1917
+ );
1918
+ `;
1919
+ /** The tables V150_SCHEMA adds. */
1920
+ export const V150_TABLES = Object.freeze(["machine_host_alias"]);
1921
+ /** v150: a process witness's machine id (saved witnesses read NULL: matched by host name, as before). */
1922
+ export const V150_ADDED_COLUMNS = Object.freeze({
1923
+ run_process: { machine_id: { definition: "TEXT CHECK (machine_id IS NULL OR length(machine_id) = 32)", saved: "NULL" } },
1924
+ });
1925
+ /** The v150 migration's steps after v149's, in order (OpenOptions.migrationStep may stop after each). */
1926
+ export const V150_STEPS = Object.freeze(["machine-aliases", "witness-machine"]);
1927
+ /** The v151 migration's steps after v150's, in order (OpenOptions.migrationStep may stop after each). */
1928
+ export const V151_STEPS = Object.freeze(["checkout-idle"]);
1929
+ /** The v152 migration's steps after v151's, in order (OpenOptions.migrationStep may stop after each). */
1930
+ export const V152_STEPS = Object.freeze(["code-map-tables", "code-map-switch", "code-map-watermark"]);
1931
+ /** v153 (plain-toolroll.md Phase 2): the tables each slice owns. */
1932
+ export const V153_SCHEMA = VANISHED_SCHEMA + CHECK_ONLY_SCHEMA + BUILDER_SERVICE_SCHEMA + FAST_PREVIEW_SCHEMA + ROUTE_CHANGE_SCHEMA;
1933
+ /** The v153 migration's steps after v152's, in order (OpenOptions.migrationStep may stop after each). */
1934
+ export const V153_STEPS = Object.freeze(["plain-tables", "plain-columns"]);
1935
+ /** The v154 migration's steps after v153's, in order (OpenOptions.migrationStep may stop after each). */
1936
+ export const V154_STEPS = Object.freeze(["check-stamps"]);
1937
+ export { V155_STEPS };
1938
+ export { V156_STEPS };
1879
1939
  /**
1880
1940
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
1881
1941
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -2382,10 +2442,10 @@ function lifecycleWords(value, cap) {
2382
2442
  const LIFECYCLE_PHASE_WORDS = {
2383
2443
  "agent-running": { subject: "agent working", body: "The agent is working in its own checkout. Nothing is finished yet." },
2384
2444
  "validating-handoff": { subject: "checking the handoff", body: "The agent finished its turn; what it handed back is being checked." },
2385
- "capturing-evidence": { subject: "capturing evidence", body: "The diff, checks and proof are being captured." },
2445
+ "capturing-evidence": { subject: "saving its files", body: "The diff, checks and report are being saved." },
2386
2446
  "committing": { subject: "committing", body: "The work is being committed on the task branch." },
2387
- "verifying-proof": { subject: "verifying the proof", body: "The proof is being checked against the approved acceptance criteria." },
2388
- "correcting-proof": { subject: "correcting the proof", body: "The proof did not match the sealed result; a bounded correction turn is running." },
2447
+ "verifying-proof": { subject: "checking the report", body: "The report is being checked against the approved requirements." },
2448
+ "correcting-proof": { subject: "correcting the report", body: "The report didn't match the saved result; one short correction turn is running." },
2389
2449
  };
2390
2450
  /** An operator's cancellation as one closed sentence: the reason, if any,
2391
2451
  * scrubbed like every phone string and ended with a full stop unless it
@@ -2398,10 +2458,10 @@ function cancellationWords(reason) {
2398
2458
  }
2399
2459
  /** What an independent review concluded, in the words the saved result shows. */
2400
2460
  const LIFECYCLE_REVIEW_WORDS = {
2401
- verified: { subject: "checks verified", body: "Every acceptance check was verified against the saved result." },
2402
- refuted: { subject: "evidence conflicts with the result", body: "The reviewer found evidence that contradicts the saved result. It is not accepted as done." },
2403
- attested: { subject: "agent-reported evidence only", body: "The result rests on agent-reported evidence that was not independently verified." },
2404
- short: { subject: "required evidence is missing", body: "Required evidence is missing from the saved result." },
2461
+ verified: { subject: "checks verified", body: "Every requirement was checked against the saved result." },
2462
+ refuted: { subject: "the report doesn't match the result", body: "The reviewer found something that contradicts the saved result. It is not accepted as done." },
2463
+ attested: { subject: "only the agent's own checks", body: "The result rests on checks the agent reported, not checked independently." },
2464
+ short: { subject: "a requirement isn't shown", body: "Something a requirement needs is missing from the saved result." },
2405
2465
  none: { subject: "comments only", body: "The reviewer left comments; no acceptance judgement was recorded." },
2406
2466
  };
2407
2467
  /** Why a task still waits after one hold lifts — the other owner, in words. */
@@ -4797,7 +4857,11 @@ function initializeStore(db, file, options = {}) {
4797
4857
  }
4798
4858
  // A current file is this build's exact shape: the checks above read, the connection gets its settings, and no DDL
4799
4859
  // runs. Everything below runs once, on the way up from an older version (a fresh file is at v107 by now).
4800
- if (preflight === SCHEMA_VERSION) {
4860
+ // A file from the first v155 builds (finding.source without 'lead') takes the v155 steps again, as a resumed epoch does.
4861
+ if (preflight === SCHEMA_VERSION && !findingSourcesStale(db)) {
4862
+ const missing = codeMapShapeProblem(db);
4863
+ if (missing !== null)
4864
+ throw new Error(`${file}: refusing to open — ${missing}, though the schema says v${SCHEMA_VERSION}`);
4801
4865
  db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
4802
4866
  return new Store(db);
4803
4867
  }
@@ -5170,6 +5234,46 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
5170
5234
  // v149: the learning-loops tables (new tables only; docs/plans/learning-loops.md). Idempotent, as v148's steps are.
5171
5235
  db.exec(V149_SCHEMA);
5172
5236
  step("learning-tables");
5237
+ // v150: this machine's stable id on each process witness and the hostnames it was seen under (V150_SCHEMA,
5238
+ // V150_ADDED_COLUMNS: saved witnesses read NULL). Idempotent, as v149's step is.
5239
+ db.exec(V150_SCHEMA);
5240
+ step("machine-aliases");
5241
+ for (const [column, { definition }] of Object.entries(V150_ADDED_COLUMNS["run_process"]))
5242
+ addColumn(db, "run_process", column, definition);
5243
+ step("witness-machine");
5244
+ // v151: the idle limit beside the cleanup setting (V151_ADDED_COLUMNS: a saved row reads NULL). Idempotent, as v150's steps are.
5245
+ for (const [column, { definition }] of Object.entries(V151_ADDED_COLUMNS["checkout_cleanup_setting"]))
5246
+ addColumn(db, "checkout_cleanup_setting", column, definition);
5247
+ step("checkout-idle");
5248
+ // v152: the code map's tables, a project's switch, and the run-id watermark, set once. Idempotent, as v151's step is.
5249
+ db.exec(V152_SCHEMA);
5250
+ step("code-map-tables");
5251
+ for (const [column, { definition }] of Object.entries(V152_ADDED_COLUMNS["project"]))
5252
+ addColumn(db, "project", column, definition);
5253
+ step("code-map-switch");
5254
+ db.exec("INSERT OR IGNORE INTO code_map_meta (id, run_watermark) SELECT 1, COALESCE(MAX(id), 0) FROM run");
5255
+ step("code-map-watermark");
5256
+ // v153: Phase 2's tables (new tables only). Idempotent, as v152's steps are.
5257
+ db.exec(V153_SCHEMA);
5258
+ step("plain-tables");
5259
+ for (const [table, columns] of Object.entries(V153_ADDED_COLUMNS))
5260
+ for (const [column, { definition }] of Object.entries(columns))
5261
+ addColumn(db, table, column, definition);
5262
+ step("plain-columns");
5263
+ // v154: each check launch's execution stamp (a new table). Idempotent, as v153's steps are.
5264
+ db.exec(V154_SCHEMA);
5265
+ step("check-stamps");
5266
+ // v155: what happens to a result after its agent finishes (new tables only). Idempotent, as v154's step is.
5267
+ db.exec(V155_SCHEMA);
5268
+ step("follow-through-tables");
5269
+ // A file from the first v155 builds keeps finding.source's CHECK without 'lead': rebuilt with every row, id and
5270
+ // counter, then its index made again. Idempotent: a widened table is untouched.
5271
+ widenFindingSources(db);
5272
+ db.exec(V155_SCHEMA);
5273
+ step("finding-sources");
5274
+ // v156: fallback chains (new tables only). Idempotent, as v155's steps are.
5275
+ db.exec(V156_SCHEMA);
5276
+ step("fallback-tables");
5173
5277
  // Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
5174
5278
  // nothing to reclaim.
5175
5279
  if (!fresh)
@@ -6381,6 +6485,8 @@ export class Store {
6381
6485
  this.db = instrumentDatabase(trackWorkspaceWrites(db), this.telemetry);
6382
6486
  // And on each instance: the sealed methods as its own locked properties, so nothing shadows one later.
6383
6487
  lockStoreMoneyMethods(this);
6488
+ // This machine's recorded names (v150), for the custody checks that hold no store (machine-identity.ts).
6489
+ rememberStoredHostAliases(this.db);
6384
6490
  }
6385
6491
  /** TESTS ONLY: raw database access for migration fixtures. Production
6386
6492
  * code never calls this — the typed methods are the API. */
@@ -6652,7 +6758,7 @@ export class Store {
6652
6758
  this.noteLifecycle({
6653
6759
  taskRef: ref.id, kind: "task-queued", identity: `t${ref.id}`,
6654
6760
  subject: "Back in the queue",
6655
- body: prior["state"] === "running" ? "Its worker released it without finishing; another attempt can start." : "Reopened for another attempt.",
6761
+ body: prior["state"] === "running" ? "Its builder let it go without finishing; another attempt can start." : "Reopened for another attempt.",
6656
6762
  link: chatControlHref("task", id),
6657
6763
  }, now);
6658
6764
  }
@@ -7249,8 +7355,9 @@ export class Store {
7249
7355
  */
7250
7356
  unhold(taskRef, mutation = {}, now) {
7251
7357
  return this.once(mutation, "unhold", () => this.transact(() => {
7358
+ // The operator's own hold, and the storage check's (dispose.ts holdForStorage): a person's "Try again".
7252
7359
  const { changes } = this.db
7253
- .prepare("DELETE FROM hold WHERE task_ref = ? AND owner_kind = 'operator'")
7360
+ .prepare("DELETE FROM hold WHERE task_ref = ? AND (owner_kind = 'operator' OR (owner_kind = 'backoff' AND owner_id = 'storage:' || task_ref))")
7254
7361
  .run(taskRef);
7255
7362
  if (Number(changes) === 0)
7256
7363
  return false;
@@ -7837,9 +7944,22 @@ export class Store {
7837
7944
  // whatever route it managed to compute (a stated problem is a term
7838
7945
  // too); only a row with no route at all keeps the caller's draft
7839
7946
  // digest, and it can never be approved.
7947
+ // THE FALLBACK CHAIN (fallback.ts): the project's agents, snapshotted here so the yes covers them. Only the road
7948
+ // that resolved its own agents: an explicit profile (a frozen flow, a pinned filing) and a deploy keep exactly those.
7949
+ let chain = null;
7950
+ if (profile !== null && options.profile === undefined && releaseTerms === null && releaseCheck === null && !scope.candidate && (ref?.deliverable ?? "branch") !== "report") {
7951
+ const baseAuth = readAuthModeStrict(profile.provider);
7952
+ const filing = baseAuth.ok ? chainForFiling(this, ref?.repo ?? null, profile, baseAuth.mode, lowered) : { ok: true, chain: null };
7953
+ if (filing.ok)
7954
+ chain = filing.chain;
7955
+ else {
7956
+ profile = null;
7957
+ unresolvedReason = filing.problem;
7958
+ }
7959
+ }
7840
7960
  const digest = profile === null
7841
7961
  ? route !== null || qualityMode === "strict" ? digestOf(digestInput, null, route) : scope.digest
7842
- : digestOf(digestInput, profile, route);
7962
+ : digestOf(digestInput, chain === null ? profile : { chain, version: LIVE_CHAIN_VERSION }, route);
7843
7963
  // THE AUTH MODE AT FILING (atomic authority closure): the credential
7844
7964
  // the resolved profile's provider would spend under is read strictly
7845
7965
  // here, on every road — explicit profile or resolved — so a present
@@ -7851,6 +7971,7 @@ export class Store {
7851
7971
  const filedAuth = readAuthModeStrict(profile.provider);
7852
7972
  if (!filedAuth.ok) {
7853
7973
  profile = null;
7974
+ chain = null;
7854
7975
  unresolvedReason = filedAuth.problem;
7855
7976
  boundDigest = digestOf(digestInput, null, route);
7856
7977
  }
@@ -7873,9 +7994,7 @@ export class Store {
7873
7994
  acceptance_json = excluded.acceptance_json, quality_mode = excluded.quality_mode,
7874
7995
  risk_level = excluded.risk_level, proposed_route_json = excluded.proposed_route_json, route_era = excluded.route_era,
7875
7996
  candidate = excluded.candidate, release_terms_json = excluded.release_terms_json, release_check_json = excluded.release_check_json`)
7876
- .run(scope.taskId, scope.goal, scope.outOfScope, JSON.stringify(scope.touches), scope.budgetMicrousd ?? null, scope.proposedAt, boundDigest, scope.approvedAt, scope.approvedBy, scope.approvedDigest, profile === null ? null : canonicalProfileJson(profile), profile === null ? "unresolved" : "resolved", unresolvedReason, profile === null ? 1 : 2, provenance,
7877
- // Fallback chains (v30-v114) are gone: no filing names one.
7878
- null, scope.acceptance.length === 0 ? null : JSON.stringify(scope.acceptance), qualityMode,
7997
+ .run(scope.taskId, scope.goal, scope.outOfScope, JSON.stringify(scope.touches), scope.budgetMicrousd ?? null, scope.proposedAt, boundDigest, scope.approvedAt, scope.approvedBy, scope.approvedDigest, profile === null ? null : canonicalProfileJson(profile), profile === null ? "unresolved" : "resolved", unresolvedReason, profile === null ? 1 : 2, provenance, chain === null ? null : chainJsonOf(chain), scope.acceptance.length === 0 ? null : JSON.stringify(scope.acceptance), qualityMode,
7879
7998
  // The declared risk level is gone (v115): every route is filed at
7880
7999
  // routine risk, the value its signed terms always carried.
7881
8000
  "routine", route === null ? null : canonicalRouteJson(route),
@@ -7947,11 +8066,17 @@ export class Store {
7947
8066
  // task — console, CLI `task scope`, and direct callers alike hit
7948
8067
  // this wall. The promise to the operator is a fresh password
7949
8068
  // ceremony that shows WHO asked; only `basis: password` seals it.
8069
+ let personReadTerms = false;
7950
8070
  if (basis !== undefined) {
8071
+ // A PERSON'S YES IN CHAT TO EVERY WORD (owner's decision, docs/plans/toolroll-cards.md): a chat seal carrying the
8072
+ // words its card showed, with the digest kept when the card was made, is a person who read them, as the
8073
+ // ceremony is. Nothing automatic (a mode, a grant, a schedule) ever gets through the walls below.
8074
+ const liveScope = basis.kind === "chat" && basis.card === "shown" ? this.getScope(taskId) : null;
8075
+ personReadTerms = liveScope !== null && typeof basis.shownTerms === "string" && basis.cardDigest === cardTermsDigest(basis.shownTerms) && cardShowsScope(liveScope, basis.shownTerms);
7951
8076
  const filedByCoordinator = this.db
7952
8077
  .prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND coordinator_cid IS NOT NULL")
7953
8078
  .get(BUILT_IN, taskId);
7954
- if (filedByCoordinator !== undefined)
8079
+ if (filedByCoordinator !== undefined && !personReadTerms)
7955
8080
  return false;
7956
8081
  // THE MATE QUARANTINE (mate arc, ruling 2): a scope a confirmed
7957
8082
  // lead proposal wrote is model-authored text; mode coverage never
@@ -7960,7 +8085,7 @@ export class Store {
7960
8085
  const writtenByLead = this.db
7961
8086
  .prepare("SELECT 1 AS hit FROM task_scope WHERE task_id = ? AND proposed_via IN ('mate','coordinator','scout')")
7962
8087
  .get(taskId);
7963
- if (writtenByLead !== undefined)
8088
+ if (writtenByLead !== undefined && !personReadTerms)
7964
8089
  return false;
7965
8090
  // THE OUTSIDE-REQUEST QUARANTINE (youiyoux-intake.ts): a task filed from a youiyoux change request is text
7966
8091
  // anyone who can write the project's folder wrote, so only a person who read every word of it approves it.
@@ -7970,12 +8095,18 @@ export class Store {
7970
8095
  // from the live scope: a card that cut a word, or terms that changed since, seal nothing.
7971
8096
  if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
7972
8097
  return false;
8098
+ // THE IMPORT QUARANTINE (github-import.ts): a task filed from a GitHub issue or pull request, or a revision of
8099
+ // one, is outside text too. Its provenance is immutable and copied through revisions, so no automatic basis
8100
+ // ever seals it, here, whatever road it came by.
8101
+ if (basis.kind !== "chat" && issueImported(this, taskId))
8102
+ return false;
7973
8103
  // CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
7974
- // all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
8104
+ // all) seals only on a person's own yes to those terms: the ceremony, or a chat card that showed every word —
8105
+ // never a mode, a grant, a plain chat tap or a carry.
7975
8106
  const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
7976
8107
  let sealedAccess;
7977
8108
  // SANDBOX WIDENINGS (v133, project-sandbox.ts): extra macOS services seal, likewise,
7978
- // only on a person's own password yes to exactly them.
8109
+ // only on a person's own yes to exactly them.
7979
8110
  let sandboxWidened = false;
7980
8111
  try {
7981
8112
  const stored = typeof profileText === "string" ? JSON.parse(profileText) : null;
@@ -7985,7 +8116,7 @@ export class Store {
7985
8116
  catch {
7986
8117
  sealedAccess = "unreadable";
7987
8118
  }
7988
- if (sealedAccess !== undefined || sandboxWidened)
8119
+ if ((sealedAccess !== undefined || sandboxWidened) && !personReadTerms)
7989
8120
  return false;
7990
8121
  }
7991
8122
  // SEPARATION OF DUTIES (v102), enforced in the primitive so no road
@@ -8048,12 +8179,15 @@ export class Store {
8048
8179
  if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
8049
8180
  return false;
8050
8181
  // The outside-request quarantine's chat road (above): the card showed every word of these live terms.
8051
- if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
8182
+ if (basis?.kind === "chat" && !personReadTerms && (this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA || issueImported(this, taskId))
8052
8183
  && !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
8053
8184
  return false;
8054
8185
  // AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
8055
8186
  // in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
8056
8187
  const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
8188
+ // A FAST START (fast-start.ts) stays a person's: no automatic release seal ever lands on one.
8189
+ if (automatic && this.db.prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND fast_start_at IS NOT NULL").get(BUILT_IN, taskId) !== undefined)
8190
+ return false;
8057
8191
  if (automatic) {
8058
8192
  if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
8059
8193
  return false;
@@ -8063,18 +8197,23 @@ export class Store {
8063
8197
  }
8064
8198
  if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
8065
8199
  return false;
8200
+ // Nor does a fast start ever anchor a release, whoever approves it.
8201
+ if (terms.terms !== null && this.db.prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND fast_start_at IS NOT NULL").get(BUILT_IN, taskId) !== undefined)
8202
+ return false;
8066
8203
  if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
8067
8204
  return false;
8205
+ // FALLBACK AGENTS (fallback.ts): a paid entry takes a person's own yes.
8206
+ if (chainSealProblem(this, readScope(working), basis) !== null)
8207
+ return false;
8068
8208
  const changed = this.db
8069
8209
  .prepare(`UPDATE task_scope
8070
8210
  SET approved_at = ?, approved_by = ?, approved_digest = digest,
8071
8211
  approved_profile_json = profile_json,
8072
8212
  approval_basis = ?, mode_digest = ?,
8073
- -- A profile approval, always (fallback-chain approvals
8074
- -- were removed in v115); a re-approval clears an old
8075
- -- chain snapshot.
8076
- approved_chain_json = NULL,
8077
- approval_kind = 'profile',
8213
+ -- The chain seals as the profile does: copied, never
8214
+ -- re-resolved. No chain clears an old snapshot.
8215
+ approved_chain_json = proposed_chain_json,
8216
+ approval_kind = CASE WHEN proposed_chain_json IS NULL THEN 'profile' ELSE 'chain' END,
8078
8217
  -- The ROUTE seals exactly as the profile does (v47):
8079
8218
  -- copied, never re-resolved.
8080
8219
  approved_route_json = proposed_route_json
@@ -8092,8 +8231,12 @@ export class Store {
8092
8231
  // (setup review): the approval is what was stale, and it is new.
8093
8232
  const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
8094
8233
  if (ref !== undefined) {
8095
- if (basis === undefined || basis.kind === "chat")
8234
+ if (basis === undefined || basis.kind === "chat") {
8096
8235
  this.noteTaskAct(Number(ref["id"]), "approved", now);
8236
+ clearRetryEpisode(this, Number(ref["id"]));
8237
+ // An ordinary yes stands on its own: a fast start's window is over (its Accept backstop stays).
8238
+ this.db.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ? AND fast_start_until IS NOT NULL").run(Number(ref["id"]));
8239
+ }
8097
8240
  this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`stale:${Number(ref["id"])}`);
8098
8241
  // Results routing: this yes closes the task's open exposure (as proposed, or with the model it sealed),
8099
8242
  // and lifts the hold a switched-off rule put on the approval it replaces.
@@ -8114,20 +8257,20 @@ export class Store {
8114
8257
  if (alreadyOn !== working["digest"]) {
8115
8258
  this.noteLifecycle({
8116
8259
  taskRef: Number(ref["id"]), kind: "scope-approved", identity: `t${Number(ref["id"])}`,
8117
- subject: "Scope approved",
8260
+ subject: "Plan approved",
8118
8261
  body: basis === undefined
8119
- ? `Approved by ${lifecycleWords(by, 40)}. A connected worker can take it next.`
8262
+ ? `Approved by ${lifecycleWords(by, 40)}. A connected builder can take it next.`
8120
8263
  : basis.kind === "chat"
8121
- ? `Approved by ${lifecycleWords(by, 40)} in chat. A connected worker can take it next.`
8264
+ ? `Approved by ${lifecycleWords(by, 40)} in chat. A connected builder can take it next.`
8122
8265
  : basis.kind === "grant"
8123
- ? "Approved automatically: its flow runs on its own. A connected worker can take it next."
8266
+ ? "Approved automatically: its flow runs on its own. A connected builder can take it next."
8124
8267
  : basis.kind === "schedule"
8125
- ? "Approved by its standing order. A connected worker can take it next."
8268
+ ? "Approved by its standing order. A connected builder can take it next."
8126
8269
  : basis.kind === "auto-check"
8127
8270
  ? "Checking automatically: it approves itself once CI and the release check pass."
8128
8271
  : basis.kind === "auto-green"
8129
8272
  ? "Approved automatically: CI and the release check passed."
8130
- : "Approved automatically under the operating mode. A connected worker can take it next.",
8273
+ : "Approved automatically under the operating mode. A connected builder can take it next.",
8131
8274
  link: chatControlHref("task", taskId),
8132
8275
  }, now);
8133
8276
  }
@@ -8166,7 +8309,7 @@ export class Store {
8166
8309
  JOIN task_ref ON task_ref.id = ? AND task_ref.backend = 'built-in' AND task_scope.task_id = task_ref.external_id
8167
8310
  WHERE task_scope.approved_at IS NOT NULL
8168
8311
  AND NOT ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
8169
- .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
8312
+ .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString(), now.toISOString());
8170
8313
  if (dead !== undefined)
8171
8314
  return false;
8172
8315
  // A release task (release-once.md): the whole live-release predicate, beyond what the SQL can say.
@@ -9706,10 +9849,7 @@ export class Store {
9706
9849
  .get(taskRef);
9707
9850
  if (ref === undefined || String(ref["backend"]) !== BUILT_IN)
9708
9851
  continue;
9709
- const { changes } = this.db
9710
- .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
9711
- .run(now.toISOString(), String(ref["external_id"]));
9712
- if (Number(changes) > 0)
9852
+ if (settleVanished(this, taskRef, now) === "queued")
9713
9853
  requeued?.push(String(ref["external_id"]));
9714
9854
  }
9715
9855
  return held.map(row => String(row["lease_id"]));
@@ -10502,7 +10642,7 @@ export class Store {
10502
10642
  this.noteLifecycle({
10503
10643
  taskRef: run.taskRef, run: runId, kind: "review-requested", identity: `r${runId}`,
10504
10644
  subject: "Independent review requested",
10505
- body: `${automatic ? "Queued automatically" : `Requested by ${lifecycleWords(by, 40)}`} for attempt #${runId}'s saved result. A connected reviewer runs it next.`,
10645
+ body: `${automatic ? "Queued automatically" : `Requested by ${lifecycleWords(by, 40)}`} for build #${runId}'s saved result. A connected reviewer runs it next.`,
10506
10646
  link: chatResultHref(reviewedTask, runId, "checks"),
10507
10647
  }, now);
10508
10648
  }
@@ -11806,6 +11946,10 @@ export class Store {
11806
11946
  for (const incident of incidents)
11807
11947
  this.resolveIncidentLocked(incident, by, now);
11808
11948
  this.resetStrikes(taskRef);
11949
+ clearRetryEpisode(this, taskRef);
11950
+ // A person's Retry is a new person act: a fast start gets a new start window. A machine's never does.
11951
+ if (!machineActing())
11952
+ this.db.prepare("UPDATE task_ref SET fast_start_until = ? WHERE id = ? AND fast_start_until IS NOT NULL").run(new Date(now.getTime() + 30 * 60_000).toISOString(), taskRef);
11809
11953
  this.db
11810
11954
  .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state IN ('failed','queued','done')")
11811
11955
  .run(stamp, taskId);
@@ -11813,10 +11957,10 @@ export class Store {
11813
11957
  this.resolveEpisode(`stalled:${taskRef}`, now);
11814
11958
  this.noteLifecycle({
11815
11959
  taskRef, kind: "task-requeued", identity: `t${taskRef}`,
11816
- subject: "Requeued",
11960
+ subject: "Queued again",
11817
11961
  body: rejectedRun !== null
11818
- ? `Its last result (attempt #${rejectedRun}) was not accepted; the next attempt continues on the same branch under the current scope.`
11819
- : `${incidents.length > 0 ? "Its incident is resolved and its" : "Its"} failure streak is cleared; a worker can take it again.`,
11962
+ ? `Its last result (build #${rejectedRun}) was not accepted; the next attempt continues on the same branch under the current plan.`
11963
+ : `Its repeated failures are cleared; a builder can take it again.`,
11820
11964
  link: chatControlHref("task", taskId),
11821
11965
  }, now);
11822
11966
  this.bumpWake();
@@ -12612,7 +12756,8 @@ export class Store {
12612
12756
  * phase's exact leg. A planner may also run under the working
12613
12757
  * proposed route, or — with no scope yet — the live recommendation,
12614
12758
  * re-derived here from durable state. Nothing else.
12615
- * fallback — never (fallback chains were removed in v115).
12759
+ * fallback — the walk's current entry of the sealed chain, exactly
12760
+ * (fallback.ts), and nothing else.
12616
12761
  * legacy — only a row proven to predate routing or a task with no
12617
12762
  * scope; never a routed row.
12618
12763
  */
@@ -12669,7 +12814,7 @@ export class Store {
12669
12814
  return "a task with no scope plans as the word legacy — nothing spends as a routed plan leg on it";
12670
12815
  }
12671
12816
  if (leg.chosen === "fallback")
12672
- return "fallback chains were removed — nothing spends as a fallback";
12817
+ return fallbackStampProblem(this, run.taskRef, taskId, leg, run.parentRun ?? null);
12673
12818
  // legacy
12674
12819
  if (leg.routeDigest !== "legacy" && !leg.routeDigest.startsWith("profile:"))
12675
12820
  return `a legacy stamp names a profile digest or the word legacy, not ${leg.routeDigest}`;
@@ -12760,8 +12905,9 @@ export class Store {
12760
12905
  return { ok: false, reason: "unreadable", detail: scope.approvedRouteJson == null ? "the approval sealed no agent route — re-file the scope and approve it again" : "the approval's sealed agent route cannot be read — re-file the scope and approve it again" };
12761
12906
  }
12762
12907
  const fields = { goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null };
12763
- const chain = scope.approvalKind === "chain" ? chainFromJson(scope.approvedChainJson ?? null) : null;
12764
- const target = chain !== null ? { chain } : (scope.approvedProfile ?? null);
12908
+ const liveChain = scope.approvalKind === "chain" ? liveChainFromJson(scope.approvedChainJson ?? null) : null;
12909
+ const chain = liveChain ?? (scope.approvalKind === "chain" ? chainFromJson(scope.approvedChainJson ?? null) : null);
12910
+ const target = chain !== null ? { chain, version: liveChain !== null ? LIVE_CHAIN_VERSION : CHAIN_DIGEST_VERSION } : (scope.approvedProfile ?? null);
12765
12911
  if (target === null)
12766
12912
  return { ok: false, reason: "unreadable", detail: "the approval sealed no agent profile — re-file the scope and approve it again" };
12767
12913
  if (digestOf(fields, target, route) !== scope.approvedDigest) {
@@ -13328,18 +13474,18 @@ export class Store {
13328
13474
  const stopped = result.outcome === "failed" && result.reason === "interrupted" && this.stopOf(id) !== null;
13329
13475
  if (role === "builder") {
13330
13476
  if (stopped) {
13331
- this.noteLifecycle({ taskRef, run: id, kind: "run-stopped", identity: `r${id}`, subject: `Attempt #${id} stopped`, body: "Its work is preserved. Resume this attempt from the task page when ready.", link: chatControlHref("recovery", taskId) }, now);
13477
+ this.noteLifecycle({ taskRef, run: id, kind: "run-stopped", identity: `r${id}`, subject: `Build #${id} stopped`, body: "Its work is preserved. Resume this attempt from the task page when ready.", link: chatControlHref("recovery", taskId) }, now);
13332
13478
  }
13333
13479
  else if (result.outcome === "built") {
13334
13480
  this.noteLifecycle({
13335
13481
  taskRef, run: id, kind: "run-finished", identity: `r${id}`,
13336
- subject: `Attempt #${id} built`,
13482
+ subject: `Build #${id} built`,
13337
13483
  body: result.committed === false ? "It finished without a commit; the handoff explains why." : "The result is saved locally. It is not yet independently reviewed or published.",
13338
13484
  link: chatResultHref(taskId, id, "changes"),
13339
13485
  }, now);
13340
13486
  }
13341
13487
  else if (result.outcome === "no-change") {
13342
- this.noteLifecycle({ taskRef, run: id, kind: "run-finished", identity: `r${id}`, subject: `Attempt #${id} finished with no changes`, body: "Nothing needed to change; the handoff explains why.", link: chatResultHref(taskId, id) }, now);
13488
+ this.noteLifecycle({ taskRef, run: id, kind: "run-finished", identity: `r${id}`, subject: `Build #${id} finished with no changes`, body: "Nothing needed to change; the handoff explains why.", link: chatResultHref(taskId, id) }, now);
13343
13489
  }
13344
13490
  return;
13345
13491
  }
@@ -13575,8 +13721,7 @@ export class Store {
13575
13721
  // route authority it holds — the sealed leg for its phase, the working
13576
13722
  // plan leg before approval, the sealed profile of a pre-routing row, the
13577
13723
  // bare word `legacy` on a task with no scope — or no row opens, in words.
13578
- // `fallback` provenance (an approved fallback entry, before v115) opens
13579
- // nothing.
13724
+ // `fallback` provenance opens only the walk's current entry.
13580
13725
  const refuse = problem => {
13581
13726
  throw new RunAdmissionRefused(`run admission refused for task_ref ${run.taskRef} (${role}): ${problem}`);
13582
13727
  };
@@ -13598,9 +13743,6 @@ export class Store {
13598
13743
  refuse("a recovered draft is a builder's");
13599
13744
  if (road.road === "repair" && run.parentRun === undefined)
13600
13745
  refuse("a repair turn mends exactly one live builder attempt — none was named");
13601
- if (run.route !== undefined && run.route.chosen === "fallback") {
13602
- refuse("fallback chains were removed — nothing opens as `fallback`");
13603
- }
13604
13746
  // EVERY run presents (atomic authority closure): a task with no scope
13605
13747
  // presents the bare word `legacy` for the pair it spends as; a task
13606
13748
  // with a scope presents the leg its authority names. Nothing opens
@@ -13834,6 +13976,12 @@ export class Store {
13834
13976
  if (Number(consumed.changes) !== 1)
13835
13977
  refuse(`review request #${requestToConsume} was consumed under this admission — nothing reviews without it`);
13836
13978
  }
13979
+ // ITS PLACE IN THE WALK (fallback.ts), in this same transaction: a run on a chain approval that can't take one rolls back.
13980
+ {
13981
+ const unbound = bindRunToWalk(this, id, run.now);
13982
+ if (unbound !== null)
13983
+ refuse(unbound);
13984
+ }
13837
13985
  // The start fact (Telegram task updates), after every proof above so a
13838
13986
  // refused admission says nothing: one per ATTEMPT — a builder, planner
13839
13987
  // or scout head, or a root reviewer. A repair turn and a correction
@@ -13841,12 +13989,12 @@ export class Store {
13841
13989
  if (road.road !== "repair" && road.road !== "correction" && !(role === "planner" && parent !== null)) {
13842
13990
  const agent = lifecycleWords(`${provider ?? "claude"}${model === null ? "" : ` · ${model}`}`, 60);
13843
13991
  const words = role === "reviewer"
13844
- ? { subject: `Independent review started (attempt ${reviewAttempt ?? 1})`, body: `Reviewing the saved result of attempt #${run.parentRun}. Running on ${agent}.`, link: chatResultHref(taskId, run.parentRun, "checks") }
13992
+ ? { subject: `Independent review started (attempt ${reviewAttempt ?? 1})`, body: `Reviewing the saved result of build #${run.parentRun}. Running on ${agent}.`, link: chatResultHref(taskId, run.parentRun, "checks") }
13845
13993
  : role === "planner"
13846
- ? { subject: `Planning started (run #${id})`, body: `Drafting the scope and plan on ${agent}.`, link: chatControlHref("planning", taskId) }
13994
+ ? { subject: "Planning started", body: `Drafting the plan on ${agent}.`, link: chatControlHref("planning", taskId) }
13847
13995
  : role === "scout"
13848
- ? { subject: `Scouting started (run #${id})`, body: `A read-only look on ${agent}; what comes back is a report.`, link: chatControlHref("task", taskId) }
13849
- : { subject: road.road === "recovered" ? `Attempt #${id} resumed from its preserved draft` : `Attempt #${id} started`, body: `Building on ${agent}.`, link: chatControlHref("task", taskId) };
13996
+ ? { subject: "Research started", body: `A read-only look on ${agent}; what comes back is a report.`, link: chatControlHref("task", taskId) }
13997
+ : { subject: road.road === "recovered" ? `Build #${id} resumed from its saved draft` : `Build #${id} started`, body: `Building on ${agent}.`, link: chatControlHref("task", taskId) };
13850
13998
  this.noteLifecycle({ taskRef: run.taskRef, run: id, kind: "run-started", identity: `r${id}`, ...words }, run.now);
13851
13999
  }
13852
14000
  return id;
@@ -14835,7 +14983,7 @@ export class Store {
14835
14983
  if (taskId === null)
14836
14984
  return;
14837
14985
  const words = LIFECYCLE_PHASE_WORDS[phase];
14838
- this.noteLifecycle({ taskRef, run: id, kind: "run-phase", identity: `r${id}`, subject: `Attempt #${id}: ${words.subject}`, body: words.body, link: chatControlHref("task", taskId) }, now);
14986
+ this.noteLifecycle({ taskRef, run: id, kind: "run-phase", identity: `r${id}`, subject: `Build #${id}: ${words.subject}`, body: words.body, link: chatControlHref("task", taskId) }, now);
14839
14987
  });
14840
14988
  }
14841
14989
  /** The accepted head and the validated conclusion, once known. */
@@ -18059,6 +18207,7 @@ export class Store {
18059
18207
  this.db.prepare(`INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail)
18060
18208
  VALUES (?,?,?,NULL,NULL,'project remote recorded','recorded','access',?)`)
18061
18209
  .run(now.toISOString(), actor, repo, JSON.stringify({ previousRemote: previous, remoteUrl }));
18210
+ codeMapRemoteChanged(this, repo);
18062
18211
  }
18063
18212
  /** The remote URL captured on admission or explicitly supplied by the owner; null when none was (or the project was removed). */
18064
18213
  projectRemote(path) {
@@ -18067,7 +18216,10 @@ export class Store {
18067
18216
  }
18068
18217
  /** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
18069
18218
  forgetProject(path) {
18070
- this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
18219
+ this.transact(() => {
18220
+ forgetCodeMap(this, path);
18221
+ this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
18222
+ });
18071
18223
  }
18072
18224
  /** Most recently opened first — the opener page's order. */
18073
18225
  listProjects() {
@@ -18758,10 +18910,7 @@ export class Store {
18758
18910
  .get(taskRef, BUILT_IN);
18759
18911
  if (ref === undefined)
18760
18912
  return;
18761
- const { changes } = this.db
18762
- .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
18763
- .run(stamp, String(ref["external_id"]));
18764
- if (Number(changes) > 0)
18913
+ if (settleVanished(this, taskRef, now) === "queued")
18765
18914
  requeued.push(String(ref["external_id"]));
18766
18915
  };
18767
18916
  const open = this.db
@@ -18847,16 +18996,11 @@ export class Store {
18847
18996
  this.db
18848
18997
  .prepare("UPDATE claim SET released_at = ?, released_by = 'recovered' WHERE lease_id = ?")
18849
18998
  .run(stamp, leaseId);
18850
- this.db
18851
- .prepare(`UPDATE task SET state = 'queued', updated_at = ?
18852
- WHERE state = 'running' AND id = (
18853
- SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
18854
- )`)
18855
- .run(stamp, taskRef, BUILT_IN);
18856
18999
  this.db
18857
19000
  .prepare(`UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ?
18858
19001
  WHERE lease_id = ? AND outcome IS NULL`)
18859
19002
  .run(stamp, leaseId);
19003
+ settleVanished(this, taskRef, now);
18860
19004
  this.db
18861
19005
  .prepare(`UPDATE worktree SET released_at = ?, verified = 0
18862
19006
  WHERE runner = ? AND task_ref = ? AND released_at IS NULL`)
@@ -18878,14 +19022,7 @@ export class Store {
18878
19022
  this.db
18879
19023
  .prepare("UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ? WHERE id = ?")
18880
19024
  .run(stamp, Number(row["run_id"]));
18881
- if (this.currentLiveLease(taskRef, now) === null) {
18882
- this.db
18883
- .prepare(`UPDATE task SET state = 'queued', updated_at = ?
18884
- WHERE state = 'running' AND id = (
18885
- SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
18886
- )`)
18887
- .run(stamp, taskRef, BUILT_IN);
18888
- }
19025
+ settleVanished(this, taskRef, now);
18889
19026
  this.db
18890
19027
  .prepare("UPDATE worktree SET released_at = ?, verified = 0 WHERE runner = ? AND task_ref = ? AND released_at IS NULL")
18891
19028
  .run(stamp, runner, taskRef);
@@ -19019,7 +19156,7 @@ export class Store {
19019
19156
  if (stoppedTask !== null) {
19020
19157
  this.noteLifecycle(run.role === "reviewer"
19021
19158
  ? { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping review #${run.id}`, body: "The review is being stopped. The saved result is unchanged.", link: chatControlHref("recovery", stoppedTask) }
19022
- : { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping attempt #${run.id}`, body: "Its processes are being stopped and its work is preserved. The task stays paused until this attempt is resumed.", link: chatControlHref("recovery", stoppedTask) }, now);
19159
+ : { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping build #${run.id}`, body: "Its processes are being stopped and its work is kept. The task stays paused until this build is resumed.", link: chatControlHref("recovery", stoppedTask) }, now);
19023
19160
  }
19024
19161
  this.bumpWake();
19025
19162
  return { ok: true, stop: this.stopOf(args.runId), repeated: false };
@@ -19078,6 +19215,7 @@ export class Store {
19078
19215
  if (ownedProcessCount(runOwnerTag(this, id)) > 0)
19079
19216
  return fact(id, "alive", `run #${id} still has an owned subprocess`);
19080
19217
  const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
19218
+ const here = storeMachine(this.db);
19081
19219
  let incompleteSpawn = false;
19082
19220
  if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
19083
19221
  return provable(id, `run #${id} may have spawned before its process witness was recorded; exit is unproven`, "no-witness");
@@ -19087,15 +19225,17 @@ export class Store {
19087
19225
  // leader. Re-probing its number could mistake reuse for the old group.
19088
19226
  if (witness["exited_at"] !== null)
19089
19227
  continue;
19090
- const host = String(witness["host"]);
19091
19228
  const bootId = witness["boot_id"] === null || witness["boot_id"] === undefined ? null : String(witness["boot_id"]);
19092
- // A VERIFIED boot change on this host (v53): nothing of the old
19229
+ // This machine by its stable id, or by one of its own names for a witness from before v150 (machine-identity.ts):
19230
+ // a hostname that moved with the network never makes this machine's own witness another computer's.
19231
+ const local = witnessIsLocal(witness, here);
19232
+ // A VERIFIED boot change on this machine (v53): nothing of the old
19093
19233
  // boot survives — an incomplete spawn, a reused pid and a native
19094
19234
  // object alike are gone. Every other case keeps the conservative
19095
19235
  // road below.
19096
- if (provenDeadByBootChange({ host, bootId }))
19236
+ if (local && provenDeadByBootChange({ host: String(witness["host"]), bootId }, { host: String(witness["host"]), bootId: currentBootId() }))
19097
19237
  continue;
19098
- if (host !== hostname())
19238
+ if (!local)
19099
19239
  return fact(id, "elsewhere", `run #${id}'s process belongs to another host; exit is unproven here`);
19100
19240
  const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
19101
19241
  const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
@@ -19152,8 +19292,9 @@ export class Store {
19152
19292
  return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action IN ('workspace settled by approver', ?) LIMIT 1").get(runId, STOP_PROOF_WORKSPACE_FREE) !== undefined;
19153
19293
  }
19154
19294
  reserveRunProcess(runId, now, group = true) {
19155
- return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
19156
- .run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
19295
+ recordMachineHostAliases(this.db, now);
19296
+ return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,machine_id) VALUES (?,?,?,?,?,?)")
19297
+ .run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId(), currentMachineId()).lastInsertRowid);
19157
19298
  }
19158
19299
  /** The run's process witnesses not yet proved exited — after first saving whatever exit evidence is available. */
19159
19300
  openRunProcessCount(runId, now) {
@@ -19173,12 +19314,13 @@ export class Store {
19173
19314
  // check uses: ESRCH, or a process now holding the pid that was born after
19174
19315
  // the run ended. An open run has no finish time, so only ESRCH counts.
19175
19316
  const finishedAt = this.getRun(runId)?.finishedAt ?? null;
19317
+ const here = storeMachine(this.db);
19176
19318
  let recorded = 0;
19177
19319
  for (const row of rows) {
19178
19320
  const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
19179
- if (host !== hostname() || typeof row["observed_at"] !== "string" || !Number.isFinite(Date.parse(row["observed_at"])) || Date.parse(row["observed_at"]) > now.getTime())
19321
+ if (!witnessIsLocal(row, here) || typeof row["observed_at"] !== "string" || !Number.isFinite(Date.parse(row["observed_at"])) || Date.parse(row["observed_at"]) > now.getTime())
19180
19322
  continue;
19181
- let gone = provenDeadByBootChange({ host, bootId });
19323
+ let gone = provenDeadByBootChange({ host, bootId }, { host, bootId: currentBootId() });
19182
19324
  if (!gone) {
19183
19325
  const backend = row["containment"] == null ? null : String(row["containment"]);
19184
19326
  const container = row["container"] == null ? null : String(row["container"]);
@@ -19189,11 +19331,12 @@ export class Store {
19189
19331
  gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, finishedAt === null ? undefined : { observedAt: row["observed_at"], finishedAt });
19190
19332
  }
19191
19333
  }
19334
+ const sameMachine = witnessesCarryMachine(this.db);
19192
19335
  if (gone)
19193
19336
  recorded += Number(this.db.prepare(`UPDATE run_process SET exited_at = ?
19194
- WHERE id = ? AND run = ? AND exited_at IS NULL AND pid IS ? AND host = ? AND process_group = ?
19337
+ WHERE id = ? AND run = ? AND exited_at IS NULL AND pid IS ? AND host = ?${sameMachine ? " AND machine_id IS ?" : ""} AND process_group = ?
19195
19338
  AND observed_at = ? AND boot_id IS ? AND containment IS ? AND container IS ? AND container_identity IS ?`)
19196
- .run(now.toISOString(), row["id"], runId, row["pid"], row["host"], row["process_group"], row["observed_at"], row["boot_id"], row["containment"], row["container"], row["container_identity"]).changes);
19339
+ .run(now.toISOString(), row["id"], runId, row["pid"], row["host"], ...(sameMachine ? [row["machine_id"] ?? null] : []), row["process_group"], row["observed_at"], row["boot_id"], row["containment"], row["container"], row["container_identity"]).changes);
19197
19340
  }
19198
19341
  return recorded;
19199
19342
  }
@@ -19203,12 +19346,13 @@ export class Store {
19203
19346
  * yet stays open and is passed over, never holding newer runs behind it:
19204
19347
  * every finished run is visited, a batch at a time. Never signals. */
19205
19348
  recordFinishedRunExits(now, batch = 200) {
19349
+ const local = localWitnessSql(this.db, "p");
19206
19350
  const page = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
19207
- WHERE p.exited_at IS NULL AND p.host = ? AND p.run > ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
19351
+ WHERE p.exited_at IS NULL AND ${local.sql} AND p.run > ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
19208
19352
  ORDER BY p.run LIMIT ?`);
19209
19353
  let recorded = 0;
19210
19354
  for (let after = 0;;) {
19211
- const runs = page.all(hostname(), after, batch).map(row => Number(row["run"]));
19355
+ const runs = page.all(...local.params, after, batch).map(row => Number(row["run"]));
19212
19356
  for (const run of runs)
19213
19357
  recorded += this.recordRunProcessExits(run, now);
19214
19358
  if (runs.length < batch)
@@ -19240,10 +19384,11 @@ export class Store {
19240
19384
  * entry. A sole pid-less witness has no group to prove gone and stays for
19241
19385
  * Toolroll's own look (stop-proof.ts). Native witnesses keep their OS-only road. */
19242
19386
  settleUnspawnedWitnesses(now, onlyRun) {
19387
+ const local = localWitnessSql(this.db, "p");
19243
19388
  const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
19244
19389
  WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
19245
- AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
19246
- AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(hostname(), onlyRun ?? null, onlyRun ?? null);
19390
+ AND ${local.sql} AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
19391
+ AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(...local.params, onlyRun ?? null, onlyRun ?? null);
19247
19392
  let settled = 0;
19248
19393
  for (const candidate of candidates) {
19249
19394
  const runId = Number(candidate["run"]);
@@ -19278,11 +19423,12 @@ export class Store {
19278
19423
  settleableUnspawnedWitnesses(run, rows) {
19279
19424
  if (run.outcome === null || run.finishedAt === null || ownedProcessCount(runOwnerTag(this, run.id)) > 0)
19280
19425
  return [];
19281
- const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && row["host"] === hostname());
19426
+ const here = storeMachine(this.db);
19427
+ const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && witnessIsLocal(row, here));
19282
19428
  const others = rows.filter(row => !pidless.includes(row));
19283
19429
  if (pidless.length === 0 || others.length === 0 || others.some(row => row["exited_at"] === null))
19284
19430
  return [];
19285
- const known = others.filter(row => row["pid"] !== null && row["host"] === hostname());
19431
+ const known = others.filter(row => row["pid"] !== null && witnessIsLocal(row, here));
19286
19432
  if (known.length === 0 || known.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] })))
19287
19433
  return [];
19288
19434
  return pidless;
@@ -19297,8 +19443,9 @@ export class Store {
19297
19443
  throw new Error("spawn witness custody changed");
19298
19444
  return;
19299
19445
  }
19300
- this.db.prepare("INSERT INTO run_process (run,pid,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?,?)")
19301
- .run(runId, pid, hostname(), group ? 1 : 0, now.toISOString(), currentBootId());
19446
+ recordMachineHostAliases(this.db, now);
19447
+ this.db.prepare("INSERT INTO run_process (run,pid,host,process_group,observed_at,boot_id,machine_id) VALUES (?,?,?,?,?,?,?)")
19448
+ .run(runId, pid, hostname(), group ? 1 : 0, now.toISOString(), currentBootId(), currentMachineId());
19302
19449
  }
19303
19450
  /** The native OS object a witnessed spawn ran inside (v53) — recorded
19304
19451
  * with the spawn, before the target can execute. */
@@ -19318,11 +19465,12 @@ export class Store {
19318
19465
  * must independently prove gone; this never grants authority to signal. */
19319
19466
  runCustodyProvenGone(runId) {
19320
19467
  const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ?").all(runId);
19468
+ const here = storeMachine(this.db);
19321
19469
  // A summarized run's witnesses all exited, each on recorded proof.
19322
19470
  return (rows.length > 0 || this.processesSummarized(runId)) && rows.every(row => {
19323
- if (String(row["host"]) !== hostname())
19471
+ if (!witnessIsLocal(row, here))
19324
19472
  return false;
19325
- if (provenDeadByBootChange({ host: String(row["host"]), bootId: row["boot_id"] == null ? null : String(row["boot_id"]) }))
19473
+ if (provenDeadByBootChange({ host: String(row["host"]), bootId: row["boot_id"] == null ? null : String(row["boot_id"]) }, { host: String(row["host"]), bootId: currentBootId() }))
19326
19474
  return true;
19327
19475
  if (row["containment"] == null || row["container"] == null)
19328
19476
  return false;
@@ -19398,7 +19546,7 @@ export class Store {
19398
19546
  this.addRunNote(args.runId, args.by, "Resumed. The next pass takes a fresh claim, re-proves the signed scope, and inherits this attempt's preserved draft with fresh proof.", now);
19399
19547
  const resumedTask = this.externalIdFor(args.taskRef);
19400
19548
  if (resumedTask !== null) {
19401
- this.noteLifecycle({ taskRef: args.taskRef, run: args.runId, kind: "run-resumed", identity: `r${args.runId}`, subject: `Attempt #${args.runId} resumed`, body: "The next pass takes a fresh claim and continues from the preserved work.", link: chatControlHref("task", resumedTask) }, now);
19549
+ this.noteLifecycle({ taskRef: args.taskRef, run: args.runId, kind: "run-resumed", identity: `r${args.runId}`, subject: `Build #${args.runId} resumed`, body: "The next attempt continues from the saved work.", link: chatControlHref("task", resumedTask) }, now);
19402
19550
  }
19403
19551
  this.bumpWake();
19404
19552
  return { ok: true, stop: this.stopOf(args.runId) };
@@ -20158,12 +20306,14 @@ export class Store {
20158
20306
  .prepare(`INSERT OR IGNORE INTO push_delivery (notification, subscription, created_at)
20159
20307
  SELECT notification.id, push_subscription.id, ?
20160
20308
  FROM notification, push_subscription
20161
- WHERE (notification.push_class IS NOT NULL OR notification.kind = 'check-progress')
20309
+ WHERE notification.push_class IS NOT NULL
20310
+ -- A fact that needs nothing from a person (checks in progress) stays in the activity log: no phone ping.
20311
+ AND notification.kind NOT IN (SELECT value FROM json_each(?))
20162
20312
  AND notification.resolved_at IS NULL
20163
20313
  AND (notification.recipient IS NULL OR notification.recipient = push_subscription.approver)
20164
20314
  AND push_subscription.retired_at IS NULL
20165
20315
  AND notification.id > push_subscription.starts_after_notification`)
20166
- .run(now.toISOString());
20316
+ .run(now.toISOString(), JSON.stringify([...ACTIVITY_ONLY_KINDS]));
20167
20317
  // Pings follow responsibility: a pair this person must not be alerted about retires before it is ever sent.
20168
20318
  if (Number(changes) > 0) {
20169
20319
  const fresh = this.db.prepare(`SELECT p.id AS pair, s.approver AS approver, n.* FROM push_delivery p
@@ -20490,7 +20640,8 @@ export class Store {
20490
20640
  return null;
20491
20641
  }
20492
20642
  }
20493
- /** The level a run's checks ran at: missing or unreadable reads as sandboxed. */
20643
+ /** The level a later check of this run may run at, at most: missing or unreadable reads as sandboxed, the narrowest.
20644
+ * Never an account of how its checks DID run: that is runCheckStamp (check-stamp.ts). */
20494
20645
  runCheckAccess(runId) {
20495
20646
  return checkAccessOf(this.db.prepare("SELECT sandbox_access FROM run WHERE id = ?").get(runId)?.["sandbox_access"]);
20496
20647
  }
@@ -21773,6 +21924,11 @@ export function carryReleaseApproval(store, w, now, recheck) {
21773
21924
  const problem = recheck();
21774
21925
  if (problem !== null)
21775
21926
  return { ok: false, why: problem };
21927
+ // How the release's check ran, read again with the write: a launch since the preflight counts (H2).
21928
+ const ran = taskCheckStamp(store, a.repo, a.tipTask);
21929
+ const unshown = ran === null ? null : stampRefusal(ran.reading, checkAccessOf(a.accessDigest));
21930
+ if (unshown !== null)
21931
+ return { ok: false, why: `This release's approval can't carry: ${unshown}.` };
21776
21932
  if (store.getTask(w.taskId) !== null)
21777
21933
  return { ok: false, why: `A task ${w.taskId} already exists.` };
21778
21934
  const anchorRow = store.handle.prepare("SELECT * FROM task_scope WHERE task_id = ?").get(a.anchorTask);
@@ -21808,7 +21964,7 @@ export function carryReleaseApproval(store, w, now, recheck) {
21808
21964
  store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: a.repo, taskId: w.taskId, runId: null, action: "release carried", outcome: "carried", source: "work",
21809
21965
  detail: `release ${a.id} · carry ${carryId} · ${a.tipSha} → ${w.to} · ${w.files} files · ${w.changedLines} lines · request ${w.requestId}` });
21810
21966
  // One quiet task notification, no push.
21811
- store.enqueueNotification({ dedupeKey: `release-carried:${a.id}:${carryId}`, kind: "release-carried", subject: `Carried to ${w.to.slice(0, 7)}`,
21967
+ store.enqueueNotification({ dedupeKey: `release-carried:${a.id}:${carryId}`, kind: "release-carried", subject: `Release now includes ${w.to.slice(0, 7)}`,
21812
21968
  body: `Release ${a.version} now ships ${w.to.slice(0, 7)} (${w.files} file${w.files === 1 ? "" : "s"}, ${w.changedLines} line${w.changedLines === 1 ? "" : "s"}) under ${anchor.approvedBy ?? "the"} release approval.`,
21813
21969
  link: chatControlHref("task", w.taskId), source: { taskRef: ref.id } }, now);
21814
21970
  return { ok: true, taskId: w.taskId, carry: releaseCarryTo(store, a.id, w.to) };
@@ -22629,7 +22785,8 @@ export function recordReleaseStep(store, entry) {
22629
22785
  .run(entry.taskId, entry.tag, entry.version, entry.pr, entry.step, entry.state, at, ended ? at : null, detail, at);
22630
22786
  }
22631
22787
  /** A release's steps, for the release task: the newest tag recorded for it, its steps in the order they ran, and the
22632
- * step it stopped at when the last thing recorded was a failure. Null: no step recorded. */
22788
+ * step it stopped at when the last thing recorded was a failure and no build of the task has started since. Null: no
22789
+ * step recorded. */
22633
22790
  export function releaseProgressFor(store, taskId) {
22634
22791
  const newest = store.handle.prepare("SELECT tag FROM release_step WHERE task_id = ? ORDER BY updated_at DESC, rowid DESC LIMIT 1").get(taskId);
22635
22792
  if (newest === undefined)
@@ -22639,9 +22796,13 @@ export function releaseProgressFor(store, taskId) {
22639
22796
  const steps = rows.map(row => ({ step: String(row["step"]), state: String(row["state"]), startedAt: String(row["started_at"]),
22640
22797
  endedAt: row["ended_at"] === null ? null : String(row["ended_at"]), detail: row["detail"] === null ? null : String(row["detail"]) }));
22641
22798
  const last = [...rows].sort((a, b) => String(a["updated_at"]).localeCompare(String(b["updated_at"]))).at(-1);
22642
- const stopped = String(last["state"]) === "failed" ? { step: String(last["step"]), message: last["detail"] === null ? "" : String(last["detail"]), at: String(last["updated_at"]) } : null;
22799
+ const failed = String(last["state"]) === "failed";
22800
+ const live = !failed ? undefined : store.handle.prepare(`SELECT id, started_at FROM run WHERE task_ref IN (SELECT value FROM json_each(?)) AND role = 'builder'
22801
+ AND finished_at IS NULL AND outcome IS NULL AND started_at > ? ORDER BY id DESC LIMIT 1`).get(JSON.stringify(taskFamilyRefs(store, taskId).map(one => one.id)), String(last["updated_at"]));
22802
+ const building = live === undefined ? null : { run: Number(live["id"]), startedAt: String(live["started_at"]) };
22803
+ const stopped = failed && building === null ? { step: String(last["step"]), message: last["detail"] === null ? "" : String(last["detail"]), at: String(last["updated_at"]) } : null;
22643
22804
  const pr = last["pr"] === null ? null : Number(last["pr"]);
22644
- return { tag, version: String(last["version"]), pr, startedAt: steps[0].startedAt, updatedAt: String(last["updated_at"]), steps, stopped };
22805
+ return { tag, version: String(last["version"]), pr, startedAt: steps[0].startedAt, updatedAt: String(last["updated_at"]), steps, stopped, building };
22645
22806
  }
22646
22807
  /** The release task whose steps were recorded most recently (v146), or null when none was. */
22647
22808
  export function latestReleaseStepTask(store) {
@@ -22878,15 +23039,17 @@ export function settleRunStopByObservation(store, args, now) {
22878
23039
  const at = now.toISOString();
22879
23040
  let what;
22880
23041
  if (args.cause === "incomplete-spawn") {
23042
+ const local = localWitnessSql(db);
22881
23043
  const changed = db.prepare(`UPDATE run_process SET exited_at = ? WHERE run = ? AND exited_at IS NULL AND pid IS NULL
22882
- AND containment IS NULL AND container IS NULL AND host = ?`).run(at, args.runId, hostname()).changes;
23044
+ AND containment IS NULL AND container IS NULL AND ${local.sql}`).run(at, args.runId, ...local.params).changes;
22883
23045
  if (Number(changed) === 0)
22884
23046
  return false;
22885
23047
  what = `${Number(changed)} process witness${Number(changed) === 1 ? "" : "es"} with no process ID recorded ended`;
22886
23048
  }
22887
23049
  else if (args.cause === "no-witness") {
22888
- db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
22889
- .run(args.runId, hostname(), at, currentBootId(), at);
23050
+ recordMachineHostAliases(db, now);
23051
+ db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at,machine_id) VALUES (?,?,0,?,?,?,?)")
23052
+ .run(args.runId, hostname(), at, currentBootId(), at, currentMachineId());
22890
23053
  what = "its missing process witness recorded ended";
22891
23054
  }
22892
23055
  else {
@@ -22901,3 +23064,112 @@ export function settleRunStopByObservation(store, args, now) {
22901
23064
  return true;
22902
23065
  });
22903
23066
  }
23067
+ /**
23068
+ * This machine as the store knows it (v150, machine-identity.ts): its stable id and every name it is known by, the
23069
+ * hostnames recorded beside its own id included. Read fresh each time, so a name recorded by another process on this
23070
+ * machine counts at once.
23071
+ */
23072
+ export function storeMachine(db) {
23073
+ const id = currentMachineId();
23074
+ let recorded = [];
23075
+ if (id !== null) {
23076
+ try {
23077
+ recorded = db.prepare("SELECT host FROM machine_host_alias WHERE machine_id = ?").all(id).map(row => String(row["host"]));
23078
+ }
23079
+ catch {
23080
+ recorded = [];
23081
+ }
23082
+ rememberHostAliases(id, recorded);
23083
+ }
23084
+ return thisMachine(recorded);
23085
+ }
23086
+ /** Load this machine's recorded names for the checks that hold no store; a store that can't read them loads none. */
23087
+ export function rememberStoredHostAliases(db) {
23088
+ try {
23089
+ storeMachine(db);
23090
+ }
23091
+ catch { /* none recorded, or no table to read: only the live names count */ }
23092
+ }
23093
+ /** Record the names this machine is known by right now beside its own id, read here at this moment (never a name a
23094
+ * witness, a note or another computer supplied). Nothing when this machine's id is unknown. */
23095
+ export function recordMachineHostAliases(db, now) {
23096
+ const id = currentMachineId();
23097
+ if (id === null)
23098
+ return;
23099
+ const names = liveHostAliases();
23100
+ const insert = db.prepare("INSERT OR IGNORE INTO machine_host_alias (machine_id, host, first_seen_at) VALUES (?, ?, ?)");
23101
+ for (const host of names)
23102
+ insert.run(id, host, now.toISOString());
23103
+ rememberHostAliases(id, names);
23104
+ }
23105
+ /** Whether a run_process row is this machine's (machine-identity.ts isThisMachine). */
23106
+ export function witnessIsLocal(row, here) {
23107
+ return isThisMachine({ host: String(row["host"]), machineId: row["machine_id"] == null ? null : String(row["machine_id"]) }, here);
23108
+ }
23109
+ /** The same rule as witnessIsLocal, as SQL over run_process (optionally aliased): its id is ours, or it has none and
23110
+ * its host is one of our names. With our id unknown, `machine_id = NULL` matches nothing: an id-bearing witness is
23111
+ * never taken for ours. */
23112
+ export function localWitnessSql(db, alias) {
23113
+ const p = alias === undefined ? "" : `${alias}.`, here = storeMachine(db);
23114
+ if (!witnessesCarryMachine(db))
23115
+ return { sql: `${p}host IN (SELECT value FROM json_each(?))`, params: [JSON.stringify(here.hosts)] };
23116
+ return { sql: `(${p}machine_id = ? OR (${p}machine_id IS NULL AND ${p}host IN (SELECT value FROM json_each(?))))`, params: [here.id, JSON.stringify(here.hosts)] };
23117
+ }
23118
+ /** Whether this database has v150's run_process.machine_id. An update settles leftover records on the database it is
23119
+ * about to back up, still at the older schema (toolroll-update.ts drain), so no query it reaches may assume the column:
23120
+ * there every witness has no id, and the host-name rule above is the whole rule. */
23121
+ export function witnessesCarryMachine(db) {
23122
+ return db.prepare("SELECT 1 FROM pragma_table_info('run_process') WHERE name = 'machine_id'").get() !== undefined;
23123
+ }
23124
+ // ---- Idle cleanup (checkout-cleanup.ts) ----------------------------------------------------------------------------
23125
+ /** Settings → Storage: how long a task stays idle before its clean folder goes (the default until someone chooses). */
23126
+ export function checkoutIdle(store) {
23127
+ const row = store.handle.prepare("SELECT idle FROM checkout_cleanup_setting WHERE id = 1").get();
23128
+ return row === undefined || row["idle"] === null ? DEFAULT_IDLE : String(row["idle"]);
23129
+ }
23130
+ /** Choose how long a task stays idle before its clean folder goes; the ledger keeps before → after. */
23131
+ export function setCheckoutIdle(store, idle, by, now) {
23132
+ return store.transact(() => {
23133
+ const before = checkoutIdle(store);
23134
+ store.handle.prepare("INSERT INTO checkout_cleanup_setting (id, cleanup, idle, updated_by, updated_at) VALUES (1, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET idle = excluded.idle, updated_by = excluded.updated_by, updated_at = excluded.updated_at")
23135
+ .run(store.checkoutCleanup(), idle, by, now.toISOString());
23136
+ if (before !== idle)
23137
+ store.recordAction({ at: now.toISOString(), actor: by, repo: null, taskId: null, runId: null, action: "idle cleanup changed", outcome: "changed", source: "policy", detail: `${idleWords(before)} → ${idleWords(idle)}` });
23138
+ return checkoutIdle(store);
23139
+ });
23140
+ }
23141
+ /** A check launch's execution stamp (check-stamp.ts), written by the code that launched it. One row per distinct launch. */
23142
+ export function recordCheckStamp(store, runId, stamp, now) {
23143
+ store.handle.prepare("INSERT OR IGNORE INTO check_stamp (run_id, digest, stamp, at) VALUES (?, ?, ?, ?)").run(runId, stampDigest(stamp), JSON.stringify(stamp), now.toISOString());
23144
+ }
23145
+ /** How a run's checks actually ran, by every launch it stamped: `unknown` when none is there or one can't be read. */
23146
+ export function runCheckStamp(store, runId) {
23147
+ return readStamps(store.handle.prepare("SELECT stamp FROM check_stamp WHERE run_id = ? ORDER BY id").all(runId).map(row => row["stamp"]));
23148
+ }
23149
+ /** The newest run of a task whose checks ran (it saved a check log), and how they ran; null when none has. */
23150
+ export function taskCheckStamp(store, repo, taskId) {
23151
+ const runs = store.handle.prepare(`SELECT r.id FROM run r JOIN task_ref t ON t.id = r.task_ref WHERE t.repo = ? AND t.external_id = ? ORDER BY r.id DESC`).all(repo, taskId);
23152
+ for (const row of runs) {
23153
+ const runId = Number(row["id"]);
23154
+ if (store.artifactsFor(runId).some(one => one.kind === "check-log"))
23155
+ return { runId, reading: runCheckStamp(store, runId) };
23156
+ }
23157
+ return null;
23158
+ }
23159
+ /** Whether this file's finding table is the first v155 builds' shape: its source CHECK without 'lead'. */
23160
+ function findingSourcesStale(db) {
23161
+ const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'finding'").get();
23162
+ return row !== undefined && String(row["sql"]).includes(FINDING_SOURCES_BEFORE_LEAD);
23163
+ }
23164
+ /** Widen finding.source to admit 'lead' (rebuildExact: every row and id, the AUTOINCREMENT counter, what refers to it,
23165
+ * in one transaction). Only the exact earlier shape is rebuilt; a widened table is untouched. */
23166
+ function widenFindingSources(db) {
23167
+ if (!findingSourcesStale(db))
23168
+ return;
23169
+ const stored = String(db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'finding'").get()["sql"]);
23170
+ if (stored.split(FINDING_SOURCES_BEFORE_LEAD).length !== 2)
23171
+ throw new Error("the finding table's DDL is not a shape this migration knows — refusing to widen it");
23172
+ const named = (sql) => (name) => sql.replace(/^CREATE TABLE (?:IF NOT EXISTS )?"?finding"?/, `CREATE TABLE ${name}`);
23173
+ const columns = db.prepare("PRAGMA table_info(finding)").all().map(one => String(one["name"]));
23174
+ rebuildExact(db, "finding", named(stored), named(stored.replace(FINDING_SOURCES_BEFORE_LEAD, FINDING_SOURCES)), columns);
23175
+ }