toolroll 0.9.92 → 0.9.94

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 (341) hide show
  1. package/CHANGELOG.md +87 -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 +4 -0
  5. package/dist/agent-fence.js +4 -1
  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 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
@@ -0,0 +1,30 @@
1
+ import { type Store } from "./store.js";
2
+ /** The automatic-retry limits, in one place. */
3
+ export declare const RETRY_LIMIT = 3;
4
+ export declare const RETRY_SPEND_TIMES = 2;
5
+ /** claim.ts MAX_STRIKES (store.ts loads this file, so the claim road isn't imported here; a test pins them equal). */
6
+ export declare const VANISHED_MAX_STRIKES = 3;
7
+ /** What recovery did with a task: back in the queue, waiting for its old process to be proven stopped, stopped for a
8
+ * person, or nothing at all (not a task of ours to move). */
9
+ export type VanishedAnswer = "queued" | "waiting" | "stopped" | "none";
10
+ export type RetryStop = "no-budget" | "attempts" | "spend";
11
+ /**
12
+ * The one recovery decision, once per run. The caller has proved the runner gone; this re-reads everything else. A
13
+ * task a tick claimed never left `queued`, so releasing its lease alone would dispatch it again: what stops a retry here
14
+ * is the state or hold this writes, in this transaction. Safe to call again: the run is charged once, decided once.
15
+ */
16
+ export declare function settleVanished(store: Store, taskRef: number, now: Date): VanishedAnswer;
17
+ /** Each sweep: a charged run whose old process wasn't yet proven stopped gets its decision now. */
18
+ export declare function retryVanishedWhenStopped(store: Store, now: Date): number;
19
+ /** A person acted, so a new episode starts. The strikes are theirs to clear on their own road. */
20
+ export declare function clearRetryEpisode(store: Store, taskRef: number): void;
21
+ /** Why Toolroll stopped retrying this task, or null: it reads as a need until a person acts. */
22
+ export declare function vanishedStopOf(store: Store, taskRef: number): {
23
+ why: RetryStop;
24
+ run: number;
25
+ } | null;
26
+ /** The need's words: the first stop on a plan with no budget, or a limit reached. */
27
+ export declare function vanishedStopSentence(stop: {
28
+ why: RetryStop;
29
+ run: number | null;
30
+ }): string;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * A build that vanished: its process died with its runner, so no one settled it (docs/plans/plain-toolroll.md P2.1).
3
+ *
4
+ * Every recovery road (a dead runner, a takeover, a superseded watch) ends here, in its own transaction. The vanished
5
+ * attempt is charged one strike, once, whoever recovers it: `vanished_charge` is keyed by the run, so a second recoverer
6
+ * or a later pass charges nothing, and restarting can no longer evade MAX_STRIKES.
7
+ *
8
+ * Toolroll retries by itself only inside an episode (`retry_episode`: the automatic retries and reserved spend since the
9
+ * last person act), at most RETRY_LIMIT retries and RETRY_SPEND_TIMES the plan's budget, each claim reserving its whole
10
+ * budget against that ceiling. A vanished attempt's spend is unknown, so its reservation is never given back. A plan
11
+ * with no budget bounds nothing and gets no automatic retry. A prepared candidate runs a machine check and no agent, so
12
+ * it spends nothing and needs no budget; it still counts its attempts.
13
+ *
14
+ * Stop, hold, archive and approval standing are re-read here, and each keeps the retry from dispatching: the task stays
15
+ * in the queue where that gate holds it, as a person's stop always has, reserving nothing, until a person acts. A
16
+ * process not yet proven stopped holds the task until it is (retryVanishedWhenStopped, every sweep): the retry never
17
+ * starts beside the old attempt.
18
+ */
19
+ import { authorityStandsSql } from "./approval-authority.js";
20
+ import { stopFactOf } from "./store.js";
21
+ import { refArchived } from "./task-archive.js";
22
+ /** The automatic-retry limits, in one place. */
23
+ export const RETRY_LIMIT = 3;
24
+ export const RETRY_SPEND_TIMES = 2;
25
+ /** claim.ts MAX_STRIKES (store.ts loads this file, so the claim road isn't imported here; a test pins them equal). */
26
+ export const VANISHED_MAX_STRIKES = 3;
27
+ /** The newest attempt that spends: repair children ride their parent's attempt, and a reviewer has no task claim. */
28
+ function attemptOf(store, taskRef) {
29
+ const row = store.handle.prepare("SELECT id, role, outcome, reason FROM run WHERE task_ref = ? AND role IN ('builder', 'scout', 'planner') ORDER BY id DESC LIMIT 1").get(taskRef);
30
+ // A planner spends no approved budget and has its own strikes: it goes back to the queue as it always has.
31
+ if (row === undefined || String(row["role"]) === "planner")
32
+ return null;
33
+ return { id: Number(row["id"]), outcome: row["outcome"] === null ? null : String(row["outcome"]), reason: row["reason"] === null ? null : String(row["reason"]) };
34
+ }
35
+ /** Whether any run of this task still has a process no record proves stopped. */
36
+ function processUnsettled(store, taskRef) {
37
+ for (const run of store.runsFor(taskRef)) {
38
+ const fact = stopFactOf(store, run.id);
39
+ if (fact !== null && fact.kind !== "open")
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+ /** dispatch.ts scopeApprovedForDispatch, read here because store.ts loads this file. */
45
+ function approvalStands(store, taskRef, now) {
46
+ const stamp = now.toISOString();
47
+ return store.handle.prepare(`SELECT 1 AS hit FROM task_scope JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
48
+ WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`).get(taskRef, stamp, stamp, stamp, stamp) !== undefined;
49
+ }
50
+ function requeue(store, taskId, now) {
51
+ return Number(store.handle.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'").run(now.toISOString(), taskId).changes) > 0;
52
+ }
53
+ function episodeOf(store, taskRef) {
54
+ const row = store.handle.prepare("SELECT retries, reserved_microusd FROM retry_episode WHERE task_ref = ?").get(taskRef);
55
+ return row === undefined ? null : { retries: Number(row["retries"]), reservedMicrousd: Number(row["reserved_microusd"]) };
56
+ }
57
+ function stopFor(store, input) {
58
+ const stamp = input.now.toISOString();
59
+ store.handle.prepare(`INSERT INTO retry_episode (task_ref, retries, reserved_microusd, stopped, stopped_run, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)
60
+ ON CONFLICT (task_ref) DO UPDATE SET stopped = excluded.stopped, stopped_run = excluded.stopped_run, reserved_microusd = excluded.reserved_microusd, updated_at = excluded.updated_at`)
61
+ .run(input.taskRef, input.retries, input.reservedMicrousd, input.why, input.run, stamp, stamp);
62
+ store.handle.prepare("UPDATE task SET state = 'failed', updated_at = ? WHERE id = ? AND state IN ('running', 'queued')").run(stamp, input.taskId);
63
+ store.enqueueNotification({
64
+ source: { run: input.run },
65
+ dedupeKey: `vanished:${input.run}`,
66
+ kind: "build-failed",
67
+ pushClass: "attention",
68
+ link: `/r/${input.run}`,
69
+ subject: `${input.taskId}: ${vanishedStopSentence({ why: input.why, run: input.run })}`,
70
+ body: "Open the task to retry it or change the plan.",
71
+ }, input.now);
72
+ return "stopped";
73
+ }
74
+ /** The custody wait's hold: nothing starts beside a process no record proves stopped, and it never lapses by itself. */
75
+ const waitHold = (taskRef) => `vanished:${taskRef}`;
76
+ /**
77
+ * The one recovery decision, once per run. The caller has proved the runner gone; this re-reads everything else. A
78
+ * task a tick claimed never left `queued`, so releasing its lease alone would dispatch it again: what stops a retry here
79
+ * is the state or hold this writes, in this transaction. Safe to call again: the run is charged once, decided once.
80
+ */
81
+ export function settleVanished(store, taskRef, now) {
82
+ return store.transact(() => {
83
+ if (store.currentLiveLease(taskRef, now) !== null)
84
+ return "none";
85
+ const ref = store.refForId(taskRef);
86
+ if (ref === null || ref.backend !== "built-in")
87
+ return "none";
88
+ const taskId = ref.externalId;
89
+ const task = store.getTask(taskId);
90
+ if (task === null)
91
+ return "none";
92
+ const attempt = attemptOf(store, taskRef);
93
+ // No attempt, or one a real outcome already settled: nothing vanished here, and recovery only unsticks the task.
94
+ if (attempt === null || (attempt.outcome !== null && attempt.reason !== "interrupted"))
95
+ return requeue(store, taskId, now) ? "queued" : "none";
96
+ // Still open: its recoverer closes it and comes back here, in the same transaction. Nothing is decided on a run that
97
+ // may not be over.
98
+ if (attempt.outcome === null)
99
+ return requeue(store, taskId, now) ? "queued" : "none";
100
+ // A person's stop isn't a vanish: nothing is charged, and its hold keeps the task where the stop always has.
101
+ if (store.handle.prepare("SELECT 1 AS hit FROM run_stop WHERE run = ?").get(attempt.id) !== undefined)
102
+ return requeue(store, taskId, now) ? "queued" : "none";
103
+ const stamp = now.toISOString();
104
+ const charged = store.handle.prepare("INSERT INTO vanished_charge (run, task_ref, charged_at) VALUES (?, ?, ?) ON CONFLICT (run) DO NOTHING").run(attempt.id, taskRef, stamp);
105
+ const strikes = Number(charged.changes) > 0 ? store.addStrike(taskRef) : Number(store.handle.prepare("SELECT strikes FROM task_ref WHERE id = ?").get(taskRef)?.["strikes"] ?? 0);
106
+ if (store.handle.prepare("SELECT settled_at FROM vanished_charge WHERE run = ?").get(attempt.id)?.["settled_at"] != null)
107
+ return "none";
108
+ const settled = () => {
109
+ store.handle.prepare("UPDATE vanished_charge SET settled_at = ? WHERE run = ?").run(stamp, attempt.id);
110
+ store.releaseOwnedHold("backoff", waitHold(taskRef));
111
+ };
112
+ if (task.state !== "queued" && task.state !== "running") {
113
+ settled();
114
+ return "none";
115
+ }
116
+ // Blocked by something a person holds: back to the queue, where that gate keeps it. Nothing is reserved, because
117
+ // nothing starts until that person acts.
118
+ const held = store.handle.prepare("SELECT 1 AS hit FROM hold WHERE task_ref = ? AND owner_kind <> 'backoff' AND (until IS NULL OR until > ?) LIMIT 1").get(taskRef, stamp);
119
+ if (held !== undefined || refArchived(store, taskRef) || !approvalStands(store, taskRef, now)) {
120
+ settled();
121
+ return requeue(store, taskId, now) || task.state === "queued" ? "queued" : "none";
122
+ }
123
+ if (processUnsettled(store, taskRef)) {
124
+ requeue(store, taskId, now);
125
+ store.holdOwned({ taskRef, ownerKind: "backoff", ownerId: waitHold(taskRef), reason: `Waiting: Toolroll is making sure build #${attempt.id} stopped.`, until: null }, now);
126
+ return "waiting";
127
+ }
128
+ const scope = store.getScope(taskId);
129
+ const budget = scope?.budgetMicrousd ?? null;
130
+ const prepared = (scope?.candidate ?? null) !== null;
131
+ const episode = episodeOf(store, taskRef) ?? { retries: 0, reservedMicrousd: budget ?? 0 };
132
+ const stop = (why) => { settled(); return stopFor(store, { taskRef, taskId, run: attempt.id, why, now, reservedMicrousd: episode.reservedMicrousd, retries: episode.retries }); };
133
+ if (budget === null && !prepared)
134
+ return stop("no-budget");
135
+ if (strikes >= VANISHED_MAX_STRIKES || episode.retries >= RETRY_LIMIT)
136
+ return stop("attempts");
137
+ const reserved = episode.reservedMicrousd + (budget ?? 0);
138
+ if (budget !== null && reserved > budget * RETRY_SPEND_TIMES)
139
+ return stop("spend");
140
+ store.handle.prepare(`INSERT INTO retry_episode (task_ref, retries, reserved_microusd, started_at, updated_at) VALUES (?, 1, ?, ?, ?)
141
+ ON CONFLICT (task_ref) DO UPDATE SET retries = retries + 1, reserved_microusd = excluded.reserved_microusd, stopped = NULL, stopped_run = NULL, updated_at = excluded.updated_at`)
142
+ .run(taskRef, reserved, stamp, stamp);
143
+ settled();
144
+ requeue(store, taskId, now);
145
+ return "queued";
146
+ });
147
+ }
148
+ /** Each sweep: a charged run whose old process wasn't yet proven stopped gets its decision now. */
149
+ export function retryVanishedWhenStopped(store, now) {
150
+ const waiting = store.handle.prepare("SELECT DISTINCT task_ref FROM vanished_charge WHERE settled_at IS NULL").all();
151
+ let moved = 0;
152
+ for (const row of waiting) {
153
+ const answer = settleVanished(store, Number(row["task_ref"]), now);
154
+ if (answer === "queued" || answer === "stopped")
155
+ moved++;
156
+ }
157
+ if (moved > 0)
158
+ store.bumpWake();
159
+ return moved;
160
+ }
161
+ /** A person acted, so a new episode starts. The strikes are theirs to clear on their own road. */
162
+ export function clearRetryEpisode(store, taskRef) {
163
+ store.handle.prepare("DELETE FROM retry_episode WHERE task_ref = ?").run(taskRef);
164
+ }
165
+ /** Why Toolroll stopped retrying this task, or null: it reads as a need until a person acts. */
166
+ export function vanishedStopOf(store, taskRef) {
167
+ const row = store.handle.prepare("SELECT stopped, stopped_run FROM retry_episode WHERE task_ref = ? AND stopped IS NOT NULL").get(taskRef);
168
+ return row === undefined ? null : { why: String(row["stopped"]), run: Number(row["stopped_run"]) };
169
+ }
170
+ /** The need's words: the first stop on a plan with no budget, or a limit reached. */
171
+ export function vanishedStopSentence(stop) {
172
+ const build = stop.run === null ? "This build" : `Build #${stop.run}`;
173
+ return stop.why === "no-budget" ? `${build} stopped unexpectedly. Retry it.` : `${build} keeps stopping. Retry, or change the plan.`;
174
+ }
@@ -77,6 +77,8 @@ export type WorkProjectCounts = {
77
77
  };
78
78
  export declare const WORK_INDEX_PAGE_LIMIT = 40;
79
79
  export declare const WORK_INDEX_MAX_LIMIT = 100;
80
+ /** The index codes that wait on nobody: finished or running work, and every wait (needs-you.ts CODE_READING). */
81
+ export declare const WAITING_CODES: readonly string[];
80
82
  /** A code's ask: a saved result is Review (unless its build may still run), otherwise its need's ask. */
81
83
  export declare function askOfCode(code: string, custody?: boolean): Ask;
82
84
  export declare class WorkIndexCursorError extends Error {
@@ -2,6 +2,7 @@
2
2
  * not verify artifacts or authorize task operations; a build Toolroll can't
3
3
  * yet confirm stopped is read through the store's own read-only probe. */
4
4
  import { authorityStandsSql } from "./approval-authority.js";
5
+ import { vanishedStopSentence } from "./vanished-retry.js";
5
6
  import { leadNameOf } from './lead-identity.js';
6
7
  import { createHash } from 'node:crypto';
7
8
  import { existsSync } from 'node:fs';
@@ -18,14 +19,15 @@ import { isCheckLevel, requiredCheckCommandFor } from './check-levels.js';
18
19
  import { MARKER } from './worktree.js';
19
20
  import { withFollowUps } from './result-follow-ups.js';
20
21
  import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
21
- import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
22
+ import { ASKS, CODE_READING, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
23
+ import { retryWhenWords } from './dispatch.js';
22
24
  import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
23
25
  export const WORK_INDEX_PAGE_LIMIT = 40;
24
26
  export const WORK_INDEX_MAX_LIMIT = 100;
25
- const EMPTY = { all: 0, 'needs-you': 0, running: 0, queued: 0, completed: 0, archived: 0 };
27
+ const EMPTY = { all: 0, 'needs-you': 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
26
28
  const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
27
29
  const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
28
- const VIEWS = ['all', 'needs-you', 'running', 'queued', 'completed', 'archived'];
30
+ const VIEWS = ['all', 'needs-you', 'ready', 'running', 'queued', 'completed', 'archived'];
29
31
  /** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
30
32
  const ARCHIVED = `EXISTS(SELECT 1 FROM task_archive ta WHERE ta.task_ref=c.ref_id)`;
31
33
  const n = (row, key) => Number(row[key] ?? 0);
@@ -39,14 +41,22 @@ const AGENT_ASK = `(SELECT q.id FROM agent_question q JOIN run qr ON qr.id=q.run
39
41
  * MATERIALIZED prevents counts + page selection repeating correlated reads. */
40
42
  /** A gate note counts only while its pause is on one of the task's current providers. */
41
43
  const GATE_PAUSE_ON_TASK = pauseOnTaskProviders('p', 'c.ref_id');
42
- /** Index codes that wait on a person, as their need (needs-you.ts); dispatch codes come from task-status's own table. */
43
- const INDEX_NEED = {
44
- 'earlier-active': 'earlier-version', 'process-needs-attention': 'confirm-stopped', 'invalid-scope': 'define-task', 'decision-queue': 'questions',
45
- };
46
- /** Every index code that can wait on a person. */
44
+ /** Index codes that wait on a person, as their need, where the index reads one the dispatch codes don't. */
45
+ const INDEX_NEED = Object.fromEntries(['earlier-active', 'process-needs-attention', 'invalid-scope', 'decision-queue', 'history-problem']
46
+ .map(code => [code, CODE_READING[code].need]));
47
+ /** Every index code that can wait on a person. A wait (stopping, waiting its turn, a retry later) is never one. */
47
48
  const PERSON_CODES = ['history-problem', 'earlier-active', 'ready-to-check', 'waiting-decision', 'decision-queue', 'result-needs-attention',
48
- 'process-needs-attention', 'failed', 'vanished-run', 'stopping', 'stopped', 'held', 'waiting-incident', 'terminal-dependency', 'needs-project',
49
+ 'process-needs-attention', 'failed', 'vanished-run', 'stopped', 'held', 'waiting-incident', 'terminal-dependency', 'needs-project',
49
50
  'needs-scope', 'invalid-scope', 'needs-agent-profile', 'needs-approval', 'missing-requirement', 'no-worker-registered', 'no-worker-online', 'signed-out'];
51
+ /** The index codes that wait on nobody: finished or running work, and every wait (needs-you.ts CODE_READING). */
52
+ export const WAITING_CODES = ['cancelled', 'complete', 'running', 'queued',
53
+ ...['stopping', 'updating', 'retry-scheduled', 'waiting-dependency', 'worker-at-capacity', 'planning-ready', 'scouting-ready'].filter(code => isWaitCode(code))];
54
+ const WAITING_SQL = WAITING_CODES.map(code => `'${code}'`).join(',');
55
+ /** The words on a state's own act (a need's act wears its need's label). */
56
+ const STATE_ACT_LABEL = {
57
+ 'ready-to-check': 'Open result', complete: 'Open result', cancelled: 'View task details', failed: 'Review and retry', 'waiting-incident': 'Review and retry',
58
+ stopped: 'Review pause', held: 'Review hold', 'result-needs-attention': 'Open the build', 'process-needs-attention': 'Open the build',
59
+ };
50
60
  /** A code's ask: a saved result is Review (unless its build may still run), otherwise its need's ask. */
51
61
  export function askOfCode(code, custody = false) {
52
62
  if (custody)
@@ -129,6 +139,8 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
129
139
  (SELECT id FROM run WHERE task_ref=c.ref_id AND lease_id=c.live_lease AND outcome IS NULL ORDER BY id DESC LIMIT 1) live_run,
130
140
  h.owner_kind hold_kind,h.reason hold_reason,h.until hold_until,
131
141
  sc.task_id scope_id,sc.digest,sc.approved_at,sc.proposed_at,sc.profile_state,
142
+ -- A prepared candidate: the machine checks out that commit and runs its check; no agent runs (machine-check.ts).
143
+ (typeof(sc.candidate)='text' AND sc.candidate<>'') machine_check,
132
144
  -- The pause the dispatch gate itself left this task waiting on (the provider it resolved, pins,
133
145
  -- configuration and planners included) first; the approved terms answer before any gate has looked.
134
146
  EXISTS(SELECT 1 FROM provider_auth_pause p WHERE p.id=c.auth_wait_pause AND p.lifted_at IS NULL AND ${GATE_PAUSE_ON_TASK}) gate_signed_out,
@@ -239,11 +251,11 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
239
251
  SELECT c.*,CASE WHEN code IN ('cancelled','complete') THEN 0 ELSE ${LEAD_CLAIM_SQL} END lead_claim FROM classified c
240
252
  ), ranked AS MATERIALIZED (
241
253
  -- A task the person's lead is on (lead-voice.ts) waits on nobody: it leaves Needs you until the claim ends or lapses.
242
- SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} AND lead_claim<>1 AND NOT ${ARCHIVED} needs,
243
- code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} AND NOT ${ARCHIVED} person_need,
254
+ SELECT c.*,code NOT IN (${WAITING_SQL}) AND NOT ${CUSTODY_WAITS} AND lead_claim<>1 AND NOT ${ARCHIVED} needs,
255
+ code NOT IN (${WAITING_SQL}) AND NOT ${CUSTODY_WAITS} AND NOT ${ARCHIVED} person_need,
244
256
  ${ARCHIVED} archived,
245
257
  CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
246
- WHEN ${CUSTODY_WAITS} OR code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
258
+ WHEN ${CUSTODY_WAITS} OR code IN (${WAITING_SQL.replace(/'(?:cancelled|complete|running)',/g, '')}) THEN 2 ELSE 0 END rank,
247
259
  ${ASK_RANK} ask_rank,
248
260
  CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
249
261
  completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest,completed.detail checked_detail
@@ -333,12 +345,14 @@ function parameters(now, access, options, custody) {
333
345
  $leadOf: access.principal === 'operator' && access.viewer != null && access.viewer !== '' ? `lead for ${access.viewer}` : null };
334
346
  }
335
347
  function counts(row) {
336
- return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), running: n(row, 'running_count'), queued: n(row, 'queued_count'), completed: n(row, 'completed_count'), archived: n(row, 'archived_count') };
348
+ return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), ready: n(row, 'ready_count'), running: n(row, 'running_count'), queued: n(row, 'queued_count'), completed: n(row, 'completed_count'), archived: n(row, 'archived_count') };
337
349
  }
338
350
  // Put-away families (v145) count only as archived.
339
351
  // Queued: waits on someone or something else (rank 2), not a person and not a build of its own family.
340
352
  const QUEUED = `rank=2 AND family_running=0 AND archived=0`;
341
- const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) needs_count,COALESCE(SUM(family_running AND archived=0),0) running_count,`
353
+ // Ready (P1.4): what waits on a person to accept or send back, the Needs you rows under Review.
354
+ const READY = `needs AND rank=0 AND ask_rank=1 AND archived=0`;
355
+ const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) needs_count,COALESCE(SUM(${READY}),0) ready_count,COALESCE(SUM(family_running AND archived=0),0) running_count,`
342
356
  + `COALESCE(SUM(${QUEUED}),0) queued_count,COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
343
357
  const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1 AND archived=0),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2 AND archived=0),0) unblock_count,`
344
358
  + `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
@@ -391,7 +405,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
391
405
  const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
392
406
  const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
393
407
  const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
394
- const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
408
+ const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'ready' ? READY : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
395
409
  const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
396
410
  SELECT * FROM ranked WHERE ($taskId IS NOT NULL OR ${filter}) AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
397
411
  ${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
@@ -400,13 +414,13 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
400
414
  SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
401
415
  SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
402
416
  )
403
- SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,
417
+ SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,'ready_count',ready_count,
404
418
  'running_count',running_count,'queued_count',queued_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
405
- UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
419
+ UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
406
420
  'archived_kind',(SELECT by_kind FROM task_archive WHERE task_ref=page.ref_id),'archived_reason',(SELECT reason FROM task_archive WHERE task_ref=page.ref_id),
407
421
  'archived_at',(SELECT at FROM task_archive WHERE task_ref=page.ref_id),'root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
408
422
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
409
- 'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),'attempt_reason',(SELECT substr(reason,1,1000) FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),
423
+ 'earlier_id',earlier_id,'route_change',(SELECT rc.words FROM route_change rc JOIN task_scope rs ON rs.task_id=page.id AND rs.digest=rc.scope_digest AND rs.approved_digest IS NOT rs.digest WHERE rc.task_ref=page.ref_id),'retry_stopped',(SELECT stopped FROM retry_episode WHERE task_ref=page.ref_id),'retry_stopped_run',(SELECT stopped_run FROM retry_episode WHERE task_ref=page.ref_id),'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),'attempt_reason',(SELECT substr(reason,1,1000) FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),
410
424
  'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
411
425
  'result_role',(SELECT role FROM run WHERE id=page_result_id),'result_head',(SELECT head_revision FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
412
426
  'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
@@ -416,11 +430,11 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
416
430
  'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
417
431
  'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
418
432
  'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'checked_detail',checked_detail,'question_id',question_id,
419
- 'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
433
+ 'dependency_id',dependency_id,'dependency_state',dependency_state,'hold_until',hold_until,'attempt_run',(SELECT id FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
420
434
  'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
421
435
  'question_run',(SELECT run FROM decision WHERE id=page.question_id),'agent_ask',page.agent_ask,
422
436
  'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
423
- 'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
437
+ 'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),'machine_check',COALESCE(machine_check,0),
424
438
  'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
425
439
  .all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
426
440
  const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
@@ -439,7 +453,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
439
453
  // automatic review and, with checks Off, the release check that covers it.
440
454
  const memo = new Map();
441
455
  const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
442
- return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
456
+ return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined, now)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
443
457
  groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), nextCursor, limit, view };
444
458
  }
445
459
  export function workCountsByProject(store, now, access) {
@@ -568,61 +582,54 @@ function resultReadinessFacts(store, now, root, row, recorded, memo) {
568
582
  const blockers = completionBlockersOf({ report, checks, checkRequired, matrix: proof?.matrix, verdict: proof?.verdict ?? null, accepted, high });
569
583
  return { checks, requirements: unverifiedWhenRefuted(requirements, verdict), ...(accepted ? { accepted } : {}), ...(blockers.length === 0 ? {} : { blockers }) };
570
584
  }
571
- function itemOf(row, principal, probe, leadName, readiness, readable = false) {
585
+ function itemOf(row, principal, probe, leadName, readiness, readable = false, now = new Date()) {
572
586
  const code = String(row['code']), id = String(row['id']);
573
587
  const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
574
588
  const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
575
- const waiting = process !== null && 'wait' in process;
589
+ const waiting = (process !== null && 'wait' in process) || (custody && probe === undefined);
576
590
  const need = n(row, 'needs') === 1 && !waiting, running = n(row, 'family_running') === 1;
577
591
  // The lead's claim (lead-voice.ts): the row still reads its own stage, worded "<name> is on it".
578
592
  const lead = n(row, 'lead_claim') === 1 ? 'on-it' : n(row, 'lead_claim') === 2 ? 'lapsed' : null;
579
593
  const personNeed = (n(row, 'person_need') === 1 || need) && !waiting;
580
594
  const assignmentState = code === 'complete' ? 'complete' : code === 'ready-to-check' ? 'ready-to-check'
581
595
  : code === 'cancelled' ? 'cancelled' : need ? 'needs-decision' : 'working';
582
- const words = {
583
- 'history-problem': ['Needs your decision', FAMILY_PROBLEM, 'inspect-task', 'Review task history'],
584
- 'earlier-active': ['Needs your decision', 'Earlier work is still active. Resolve it before completing this assignment.', 'inspect-task', 'Review active work'],
585
- 'ready-to-check': ['Ready', 'The saved result is ready to review.', 'open-result', 'Open result'],
586
- complete: ['Complete', 'Completion is recorded for this result. Open it to inspect the saved evidence.', 'open-result', 'Open result'],
587
- cancelled: ['Cancelled', 'This assignment was cancelled.', 'inspect-task', 'View assignment'],
588
- running: ['Running now', '', null, ''],
589
- 'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : n(row, 'agent_ask') ? 'answer-agent' : 'inspect-decisions', 'Answer question'],
590
- 'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
591
- // Its own reason replaces this below (resultHoldUpSentence).
592
- 'result-needs-attention': ['Needs your decision', '', 'inspect-run', 'Inspect run'],
593
- 'process-needs-attention': ['Needs your decision', 'A process exit is not recorded. Open the attempt to check whether its work has stopped.', 'inspect-run', 'Inspect run'],
594
- failed: ['Needs your decision', 'The last attempt stopped.', 'retry-task', 'Review and retry'],
595
- 'vanished-run': ['Needs your decision', 'An unfinished attempt has no current live claim.', 'reconcile-run', 'Check the unfinished attempt'],
596
- stopping: ['Needs your decision', 'Stopping. The work so far is kept.', 'inspect-stop', 'View stop details'],
597
- stopped: ['Needs your decision', 'Paused by a person. The work so far is kept.', 'resume-run', 'Review pause'],
598
- held: ['Needs your decision', s(row, 'hold_reason') ?? 'This task is on hold.', s(row, 'hold_kind') === 'operator' ? 'unhold' : 'inspect-hold', 'Review hold'],
599
- 'waiting-incident': ['Needs your decision', 'An unresolved incident holds the next attempt.', 'retry-task', 'Review and retry'],
600
- 'terminal-dependency': ['Needs your decision', 'A required task did not finish.', 'repair-dependency', 'Review required task'],
601
- 'waiting-dependency': ['Waiting for another task', 'A required task must finish before this task can start.', null, ''],
602
- 'needs-project': ['Needs your decision', 'Choose a project for this task.', 'place-task', 'Choose a project'],
603
- 'needs-scope': ['Needs your decision', 'Define the task before a worker can start.', 'write-scope', 'Define the task'],
604
- 'invalid-scope': ['Needs your decision', 'The saved task scope cannot be read safely.', 'write-scope', 'Review scope'],
605
- 'needs-agent-profile': ['Needs your decision', 'Choose an available agent for this task.', 'select-agent', 'Choose an agent'],
606
- 'needs-approval': ['Needs your decision', 'Review the current plan before a worker can start.', 'approve-scope', 'Approve plan'],
607
- 'missing-requirement': ['Needs your decision', 'A required capability is missing or expired.', 'repair-capability', 'Review missing requirement'],
608
- 'no-worker-registered': ['Needs your decision', 'No connected builder includes this project.', 'start-worker', 'Check connection'],
609
- 'no-worker-online': ['Needs your decision', 'The builder has stopped checking in.', 'start-worker', 'Check connection'],
610
- 'worker-at-capacity': ['Waiting for worker capacity', 'This task starts when an eligible worker has capacity.', null, ''],
611
- 'signed-out': ['Waiting for a sign-in', 'Its agent needs you to sign in again; it starts on its own once that works.', null, ''],
612
- 'retry-scheduled': ['Retry scheduled', 'The attempt is backing off before retrying.', null, ''],
613
- updating: ['Waiting for app update', 'New work resumes after the update.', null, ''],
614
- 'planning-ready': ['Planner ready', 'A connected worker can draft the plan.', null, ''],
615
- 'scouting-ready': ['Scout ready', 'A connected worker can produce the report.', null, ''],
616
- queued: ['Queued', '', null, ''],
617
- };
618
- let [label, detail, actionCode, actionLabel] = words[code];
596
+ // What the code means (needs-you.ts CODE_READING, the one table): its act is the one it has always had; a need's
597
+ // words and label come from NEEDS, a wait's from WAITS. Only facts the row itself holds are added here.
598
+ const meaning = CODE_READING[code];
599
+ let label = '';
600
+ let detail = '';
601
+ let actionCode = (meaning?.act ?? null);
602
+ let actionLabel = actionCode === null ? '' : STATE_ACT_LABEL[code] ?? (meaning?.need === undefined ? '' : NEEDS[meaning.need].action.label);
603
+ if (code === 'waiting-decision')
604
+ actionCode = n(row, 'question_id') ? 'answer-decision' : n(row, 'agent_ask') ? 'answer-agent' : 'inspect-decisions';
605
+ if (code === 'held') {
606
+ actionCode = s(row, 'hold_kind') === 'operator' ? 'unhold' : 'inspect-hold';
607
+ detail = s(row, 'hold_reason') ?? 'This task is on hold.';
608
+ }
609
+ if (code === 'stopping') {
610
+ actionCode = 'inspect-stop';
611
+ actionLabel = 'View stop details';
612
+ }
613
+ if (code === 'stopped')
614
+ detail = 'Paused by a person. The work so far is kept.';
615
+ if (code === 'cancelled')
616
+ detail = 'This task was cancelled.';
617
+ // The same failure again and again: what failed, and the two ways on.
618
+ if (code === 'waiting-incident')
619
+ detail = `The last attempts failed the same way (${failedAttemptSentence(s(row, 'attempt_reason')).replace(/[.\s]+$/, '').replace(/^./, first => first.toLowerCase())}). Retry, or change the plan.`;
620
+ /** The build, the task or the time a sentence names. */
621
+ const context = code === 'stopping' ? { build: n(row, 'stop_run') || null }
622
+ : code === 'retry-scheduled' ? { build: n(row, 'attempt_run') || null, retry: s(row, 'hold_until') === null ? null : retryWhenWords(s(row, 'hold_until'), now) }
623
+ : code === 'vanished-run' ? { build: n(row, 'unfinished') || null }
624
+ : code === 'earlier-active' ? { task: s(row, 'earlier_id') }
625
+ : code === 'waiting-dependency' ? { task: s(row, 'dependency_id') } : {};
619
626
  if (code === 'signed-out') {
620
627
  const [provider = '', authMode = 'subscription'] = (s(row, 'signed_out') ?? '').split(':');
621
628
  label = signInReason({ provider: provider, authMode: authMode === 'api-key' ? 'api-key' : 'subscription' });
622
629
  detail = `${label}. It starts again on its own once ${providerName(provider)} works.`;
623
630
  }
624
631
  if (code === 'running' && n(row, 'version_count') > 1)
625
- label = 'Revising';
632
+ label = 'Building again';
626
633
  // Replaced, never "Cancelled": the successor is one tap away.
627
634
  const successor = code === 'cancelled' ? s(row, 'replaced_by') : null;
628
635
  if (successor !== null) {
@@ -633,7 +640,11 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
633
640
  detail = `${s(row, 'dependency_id')} ${s(row, 'dependency_state') === 'cancelled' ? 'was cancelled' : 'failed'} before it finished.`;
634
641
  // A failed task says the latest finished attempt across its whole family, whatever its outcome: its recorded reason, or that none was recorded.
635
642
  if (code === 'failed')
636
- detail = failedAttemptSentence(s(row, 'attempt_reason'));
643
+ detail = s(row, 'retry_stopped') === null ? failedAttemptSentence(s(row, 'attempt_reason'))
644
+ : vanishedStopSentence({ why: s(row, 'retry_stopped'), run: n(row, 'retry_stopped_run') || null });
645
+ // A new route waiting for its one Approve says what changed (route-change.ts).
646
+ if (code === 'needs-approval' && s(row, 'route_change') !== null)
647
+ detail = s(row, 'route_change');
637
648
  // A saved result that can't be accepted yet says its own reason, never one line for every such row.
638
649
  const planChanged = code === 'result-needs-attention' && n(row, 'plan_changed') === 1;
639
650
  if (code === 'result-needs-attention')
@@ -647,14 +658,11 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
647
658
  actionCode = 'open-result';
648
659
  actionLabel = 'Open result';
649
660
  }
661
+ // Toolroll proves a build stopped itself; only a stop it can't prove from here offers "Check again", never a vouch.
650
662
  if (process !== null && 'need' in process) {
651
- actionCode = 'confirm-stopped';
663
+ actionCode = 'recheck-stopped';
652
664
  actionLabel = NEEDS[process.need].action.label;
653
665
  }
654
- else if (custody && probe === undefined) {
655
- actionCode = 'confirm-stopped';
656
- actionLabel = NEEDS['confirm-stopped'].action.label;
657
- }
658
666
  if (code === 'signed-out') {
659
667
  actionCode = 'sign-in';
660
668
  actionLabel = NEEDS['sign-in'].action.label;
@@ -668,7 +676,7 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
668
676
  target: { taskId: successor ?? (code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id),
669
677
  runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'answer-agent' ? n(row, 'agent_ask_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
670
678
  : actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null
671
- : actionCode === 'confirm-stopped' ? process?.build ?? (n(row, 'result_id') || null) : null,
679
+ : actionCode === 'recheck-stopped' ? process?.build ?? (n(row, 'result_id') || null) : null,
672
680
  decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
673
681
  access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'answer-agent', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
674
682
  retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
@@ -676,7 +684,8 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
676
684
  const coded = stageOfCode(code, { needsPerson: personNeed, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
677
685
  const reading = process !== null
678
686
  ? 'wait' in process ? { stage: 'waiting', wait: process.wait } : { stage: 'needs-you', need: process.need }
679
- : custody ? { stage: 'needs-you', need: 'confirm-stopped' }
687
+ // Not read here (no process probe): Toolroll's own stop check settles it, so it waits on no one.
688
+ : custody ? { stage: 'waiting', wait: 'build-checking' }
680
689
  : coded.stage === 'needs-you' && (coded.need === undefined || coded.need === 'other') && INDEX_NEED[code] !== undefined ? { stage: 'needs-you', need: INDEX_NEED[code] } : coded;
681
690
  const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
682
691
  const finished = reading.stage === 'finished' || reading.stage === 'complete';
@@ -689,8 +698,9 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
689
698
  // Toolroll marked it complete because its work landed: the reason, said once.
690
699
  const landed = code === 'complete' ? landedReasonOf(s(row, 'checked_actor'), s(row, 'checked_detail')) : null;
691
700
  const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
692
- ...(process === null ? {} : { needContext: { build: process.build } }),
701
+ ...(process === null ? (Object.keys(context).length === 0 ? {} : { needContext: context }) : { needContext: { build: process.build } }),
693
702
  reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
703
+ ...(n(row, 'machine_check') === 1 ? { machine: true } : {}),
694
704
  checks: ready !== null ? ready.checks : listedChecks,
695
705
  ...(ready === null ? {} : { requirements: ready.requirements ?? null, ...(ready.accepted ? { accepted: true } : {}), ...(ready.blockers === undefined ? {} : { blockers: ready.blockers }) }),
696
706
  completedBy: code !== 'complete' || landed !== null ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
@@ -716,6 +726,8 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
716
726
  const views = archived !== null ? ['archived'] : ['all'];
717
727
  if (archived === null && need)
718
728
  views.push('needs-you');
729
+ if (archived === null && need && n(row, 'rank') === 0 && n(row, 'ask_rank') === 1)
730
+ views.push('ready');
719
731
  if (archived === null && running)
720
732
  views.push('running');
721
733
  if (archived === null && !need && !running && n(row, 'rank') === 2)
@@ -8,9 +8,9 @@ export type WorkTarget = {
8
8
  decisionId: number | null;
9
9
  };
10
10
  export type WorkAction = {
11
- /** `confirm-stopped`: an approver confirms a finished build stopped (behind the password);
11
+ /** `recheck-stopped`: Toolroll looks again whether a finished build stopped (nobody vouches for it);
12
12
  * `sign-in`: the agent's sign-in, in Settings; `answer-agent`: Reply to a running agent's question on the Agents board. */
13
- code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "confirm-stopped" | "sign-in" | "answer-agent";
13
+ code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "recheck-stopped" | "sign-in" | "answer-agent";
14
14
  label: string;
15
15
  target: WorkTarget;
16
16
  access: "read" | "operator-control" | "proposal-only" | "operator-handoff";
@@ -2,6 +2,7 @@
2
2
  * State, decisions, approval, process custody and proof still belong to
3
3
  * their existing owners. Action hints are navigation/proposal guidance,
4
4
  * never credentials or permission to bypass an owning operation. */
5
+ import { machineChecked } from "./machine-check.js";
5
6
  import { resultCheckLevel } from "./result-follow-ups.js";
6
7
  import { diagnoseTaskDispatch } from "./dispatch.js";
7
8
  import { isAlive } from "./runner.js";
@@ -94,6 +95,7 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
94
95
  const publication = result === null ? null : store.publicationForRun(result.id);
95
96
  const facts = {
96
97
  id: task.id, title: task.title, repo: ref.repo, state: task.state, updatedAt: task.updatedAt,
98
+ ...(machineChecked(store, task.id) ? { machine: true } : {}),
97
99
  dispatch: diagnoseTaskDispatch(store, taskId, now, access.dependencyRepos ?? null), liveRunId: live?.id ?? null,
98
100
  unfinishedRunId: runs.find(one => one.outcome === null && one.role !== "reviewer")?.id ?? null,
99
101
  control: taskControlOf(store, ref.id, now), openDecision: openWorkDecisionOf(store, ref.id, now),
@@ -26,7 +26,7 @@ import { type ChecksFact } from "./task-status.js";
26
26
  import { type Html } from "./html.js";
27
27
  /** The Work destination's views — shortcuts over the same rows, never a
28
28
  * persisted state. All is the default. */
29
- export type WorkView = "all" | "needs-you" | "running" | "queued" | "completed" | "archived";
29
+ export type WorkView = "all" | "needs-you" | "ready" | "running" | "queued" | "completed" | "archived";
30
30
  export declare const WORK_VIEWS: readonly {
31
31
  key: WorkView;
32
32
  label: string;
@@ -154,6 +154,8 @@ export declare function receiptPublicationWords(publication: PublicationFacts):
154
154
  * already reads elsewhere. */
155
155
  export type WorkFacts = {
156
156
  id: string;
157
+ /** The machine runs this task's check on a prepared candidate; no agent runs (machine-check.ts). */
158
+ machine?: boolean;
157
159
  title: string;
158
160
  repo: string | null;
159
161
  state: TaskState;
@@ -233,7 +235,7 @@ export declare function lastErrorLineOf(log: string): {
233
235
  text: string;
234
236
  } | null;
235
237
  /** The result page's own sentence when the decision is on it. */
236
- export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
238
+ export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or send it back.";
237
239
  /** What the result page's Accept says, from facts it already shows: the
238
240
  * recorded checks, the requirements not met, whether the saved proof reads,
239
241
  * and what the button posts. "Accept and finish" only when everything is met