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
@@ -16,22 +16,25 @@
16
16
  * observed" name observed publication records, and deployment is never
17
17
  * claimed without one.
18
18
  */
19
+ import { lastIncludedWords } from './memory-use.js';
19
20
  import { ACCEPT_NEEDS_REASON, FAILED_CHECK, cantAcceptYetOf, evidenceProblemOf } from "./result-acts.js";
20
21
  export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf };
21
22
  import { manualReviewOnly, plainReasonWords } from "./proof.js";
22
23
  import { CHECK_COMMAND_CHANGED, CHECK_UNREADABLE, OPEN_RESULT, approvedCheckOf, approvedCheckStateOf, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
23
24
  import { whenUtc } from "./when-html.js";
25
+ import { NEEDS } from "./needs-you.js";
24
26
  import { html, postForm } from "./html.js";
25
27
  export const WORK_VIEWS = [
26
28
  { key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
27
29
  { key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
30
+ { key: "ready", label: "Ready", hint: "Results waiting for you to accept or send back.", empty: "Nothing is ready for review." },
28
31
  { key: "running", label: "Running", hint: "Attempts a builder owns right now.", empty: "Nothing is running right now. Approved tasks start when a builder with capacity is connected." },
29
32
  { key: "queued", label: "Queued", hint: "Tasks that start on their own: waiting for a builder, a dependency or a retry.", empty: "Nothing is queued." },
30
- { key: "completed", label: "Finished", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "Nothing has finished in this view yet." },
31
- { key: "archived", label: "Archived", hint: "Tasks put away by you or by tidy. Their history and results are kept.", empty: "Nothing is put away." },
33
+ { key: "completed", label: "Finished", hint: "Tasks the lead or you accepted. Recorded check results stay available.", empty: "Nothing has finished in this view yet." },
34
+ { key: "archived", label: "Archived", hint: "Tasks archived by you or automatically. Their history and results are kept.", empty: "Nothing is archived." },
32
35
  ];
33
36
  export function parseWorkView(raw) {
34
- return raw === "needs-you" || raw === "running" || raw === "queued" || raw === "completed" || raw === "archived" ? raw : "all";
37
+ return raw === "needs-you" || raw === "ready" || raw === "running" || raw === "queued" || raw === "completed" || raw === "archived" ? raw : "all";
35
38
  }
36
39
  /** The review facts from the store's bounded retry projection — the same
37
40
  * reading `diagnoseTaskDispatch` makes, so every surface starts from one
@@ -141,8 +144,8 @@ function machineVerdictWords(result, check) {
141
144
  return `the approved check failed against it${exit === null ? "" : ` (exit ${exit})`}`;
142
145
  }
143
146
  if (problem === "mismatched")
144
- return "its evidence did not match the sealed record";
145
- return "its required evidence was missing";
147
+ return "its saved files didn't match the record";
148
+ return "its required files were missing";
146
149
  }
147
150
  /** The approved check passed (task-status.ts approvedCheckStateOf): its own pass, or checks Off covered by a passing
148
151
  * release check. A re-run underway keeps the pass it had. */
@@ -180,7 +183,7 @@ function storedResultStatusOf(result, publication) {
180
183
  // Acceptance records a person's decision; it neither proves nor
181
184
  // disproves anything about the checks. The machine's own verdict is
182
185
  // restated as recorded.
183
- detail: withPublication(`An approver accepted this result by hand, and the recorded exception says why. The machine's verdict is unchanged: ${machineVerdictWords(result, check)}.`),
186
+ detail: withPublication(`An approver accepted this result by hand, and their reason says why. The checks are unchanged: ${machineVerdictWords(result, check)}.`),
184
187
  tone: "done",
185
188
  action: { label: "Open the recorded exception", kind: "open-review" },
186
189
  };
@@ -228,16 +231,16 @@ function storedResultStatusOf(result, publication) {
228
231
  // at adjudication; only a verdict-less no-change reads its two
229
232
  // presence facts here.
230
233
  return result.recordComplete === false && result.verdict === null
231
- ? { token: "record-incomplete", label: "No-change result, record incomplete", detail: "The build concluded nothing needed to change, but its handoff or sealed diff is missing.", tone: "problem", action: { label: "Open the record", kind: "open-run" } }
232
- : { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and sealed diff are on record."), tone: "done", action: { label: OPEN_RESULT, kind: "open-result" } };
234
+ ? { token: "record-incomplete", label: "No-change result, record incomplete", detail: "The build concluded nothing needed to change, but its handoff or saved diff is missing.", tone: "problem", action: { label: "Open the record", kind: "open-run" } }
235
+ : { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and saved diff are on record."), tone: "done", action: { label: OPEN_RESULT, kind: "open-result" } };
233
236
  }
234
237
  if (result.verdict === null) {
235
238
  return {
236
239
  token: "verification-needed",
237
- label: "Result saved — verification needed",
238
- detail: withPublication("No verification result is recorded for this build."),
240
+ label: "No check result for this build yet",
241
+ detail: withPublication("No check result for this build yet. Run checks on it."),
239
242
  tone: "problem",
240
- action: { label: "See what is missing", kind: "open-review" },
243
+ action: { label: "Run checks", kind: "open-review" },
241
244
  };
242
245
  }
243
246
  // Any verdict short of a pass the check confirms: never "passed" when the check can't be read now.
@@ -250,8 +253,8 @@ function storedResultStatusOf(result, publication) {
250
253
  if (result.verdict === "attested" && !passedNow(check)) {
251
254
  return {
252
255
  token: "agent-attested",
253
- label: "Result saved — checks reported by the agent",
254
- detail: withPublication("No independent project check ran; the checks listed are the agent's own report."),
256
+ label: "Ready · no project check set up",
257
+ detail: withPublication("No project check is set up, so the checks listed are the agent's own report."),
255
258
  tone: "neutral",
256
259
  action: { label: OPEN_RESULT, kind: "open-result" },
257
260
  };
@@ -322,11 +325,14 @@ const DISPATCH_ACTION_LABELS = {
322
325
  "inspect-hold": "Review hold",
323
326
  "repair-dependency": "Review required task",
324
327
  "repair-capability": "Review missing requirement",
325
- "start-worker": "Check connection",
328
+ "start-worker": "Start the builder",
326
329
  "retry-review": "Review retry options",
327
330
  "resume-run": "Review pause",
328
331
  };
329
332
  export function dispatchActionLabel(dispatch) {
333
+ // One label everywhere for the builder (needs-you.ts NEEDS): Connect a builder, or Start the builder.
334
+ if (dispatch?.action === "start-worker")
335
+ return dispatch.code === "no-worker-registered" ? NEEDS["connect-builder"].action.label : NEEDS["start-builder"].action.label;
330
336
  return dispatch?.action == null ? "View task details" : DISPATCH_ACTION_LABELS[dispatch.action];
331
337
  }
332
338
  /** Result tokens a done task can wear, as the shared stage they mean. A
@@ -376,7 +382,7 @@ export function reportMismatchesOf(reasons, criteria, changes) {
376
382
  return [{ text: plainReasonWords(reason), ...place(review[1]), note: null, reason }];
377
383
  const unsigned = UNSIGNED_NOTE.exec(reason);
378
384
  if (unsigned !== null)
379
- return [{ text: `The report's note ${unsigned[1]} is about ${unsigned[2]}, which isn't one of the signed requirements: ${unsigned[3]}`, path: null, lines: null, inChanges: null, note: Number(unsigned[1]), reason }];
385
+ return [{ text: `The report's note ${unsigned[1]} is about ${unsigned[2]}, which isn't one of the approved requirements: ${unsigned[3]}`, path: null, lines: null, inChanges: null, note: Number(unsigned[1]), reason }];
380
386
  if (FAILED_CHECK.test(reason))
381
387
  return [];
382
388
  return [{ text: sentenceOf(plainReasonWords(reason)), path: null, lines: null, inChanges: null, note: note === null ? null : Number(note), reason }];
@@ -419,7 +425,7 @@ export function lastErrorLineOf(log) {
419
425
  return (stderr >= 0 ? pick(stderr + 1, lines.length) : null) ?? pick(0, stderr >= 0 ? stderr : lines.length);
420
426
  }
421
427
  /** The result page's own sentence when the decision is on it. */
422
- export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
428
+ export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or send it back.";
423
429
  export function acceptWordsOf(facts) {
424
430
  const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
425
431
  const parts = [
@@ -435,7 +441,7 @@ export function acceptWordsOf(facts) {
435
441
  : facts.publishing === "pull-request" ? "Finishes the task. No pull request opens; you can open one from the task after."
436
442
  : facts.publishing === "off" ? "Finishes the task. The branch stays; publishing isn't set up."
437
443
  : "Finishes the task. Nothing is published.";
438
- return { label: ready ? "Accept and finish" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
444
+ return { label: ready ? "Accept" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
439
445
  }
440
446
  export function resultHeadlineOf(status) {
441
447
  const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
@@ -454,8 +460,8 @@ export function workStatusOf(facts, resultDisplay) {
454
460
  : stageOfCode(status.token, { needsPerson: status.views.includes("needs-you"), planning: facts.dispatch?.code === "running" && facts.dispatch.role === "planner", operatorHold: facts.dispatch?.action === "unhold" });
455
461
  const stage = done && read.stage !== "needs-you" && read.stage !== "failed" ? "finished" : read.stage;
456
462
  const level = facts.result?.checkLevel ?? null;
457
- const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), reason: plainReasonOf(stage, status.token, status.detail),
458
- report: facts.result?.role === "scout", checks: stage !== "finished" ? null : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null, level }
463
+ const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), ...(read.wait === undefined ? {} : { wait: read.wait }), reason: plainReasonOf(stage, status.token, status.detail),
464
+ report: facts.result?.role === "scout", ...(facts.machine === true ? { machine: true } : {}), checks: stage !== "finished" ? null : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null, level }
459
465
  : level === "off" ? { status: "not-run", exitCode: null, head: null, level } : null });
460
466
  return { ...status, label: shared.headline, tone: workToneOf(shared.headline), detail: stage === "finished" && level !== "off" ? status.detail : shared.sentence };
461
467
  }
@@ -486,7 +492,7 @@ function workStatusWordsOf(facts, resultDisplay) {
486
492
  // The control projection knows whether a stop has actually settled.
487
493
  // A still-live claim alone cannot distinguish Running from Stopping.
488
494
  if (facts.control?.kind === "stopping") {
489
- return { token: "stopping", label: "Stopping…", detail: facts.control.detail ?? "The attempt is ending. Its work is preserved; resume becomes available after its processes exit.", tone: "attention", action: { label: "View stop details", kind: "open-task" }, views, rank: 0 };
495
+ return { token: "stopping", label: "Stopping…", detail: facts.control.detail ?? "The attempt is ending. Its work is preserved; resume becomes available after its processes exit.", tone: "attention", action: { label: "View stop details", kind: "open-task" }, views, rank: 1 };
490
496
  }
491
497
  if (facts.control?.kind === "paused") {
492
498
  return { token: "stopped", label: "Paused", detail: "The stopped attempt's work is preserved. Resuming requires confirmation and checks the current approval again.", tone: "attention", action: { label: "Review pause", kind: "open-task" }, views, rank: 0 };
@@ -529,7 +535,7 @@ function workStatusWordsOf(facts, resultDisplay) {
529
535
  }
530
536
  /** The counts each view tab wears — from the same rows the page lists. */
531
537
  export function workCounts(rows) {
532
- const counts = { all: 0, "needs-you": 0, running: 0, queued: 0, completed: 0, archived: 0 };
538
+ const counts = { all: 0, "needs-you": 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
533
539
  for (const row of rows)
534
540
  for (const view of row.views)
535
541
  counts[view] += 1;
@@ -557,7 +563,7 @@ export function learningHtml(view, csrf, canManage, source) {
557
563
  const act = (action, label, lesson) => canManage && csrf ? postForm('/settings/learning/change', html `<button type="submit">${label}</button>`, { hidden: { ...base, action, ...(lesson ? { lesson: lesson.id, version: lesson.version, sha: lesson.sha } : {}) } }) : '';
558
564
  const evidence = (list, run) => list.map(one => html `<li><a href="/r/${String(one.runId ?? run)}/evidence/${one.artifactId}">Source #${one.artifactId}</a><blockquote>${one.excerpt}</blockquote><code>${one.sha256}</code></li>`);
559
565
  const lessons = view.lessons.filter(l => source === undefined || l.source === source);
560
- const cards = lessons.map(l => html `<article class="card" data-lesson="${l.id}"><p><strong>${l.payload.observation}</strong></p><p>${l.payload.action}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson'}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${l.payload.paths.join(', ')} · ${l.payload.phases.join(', ')} · ${{ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : html `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`);
566
+ const cards = lessons.map(l => html `<article class="card" data-lesson="${l.id}"><p><strong>${l.payload.observation}</strong></p><p>${l.payload.action}</p><p class="meta">${lastIncludedWords(view.use.get(`lesson:${l.id}`))}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson'}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${l.payload.paths.join(', ')} · ${l.payload.phases.join(', ')} · ${{ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : html `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`);
561
567
  if (source !== undefined)
562
568
  return lessons.length ? html `<details class="learning result-learning"><summary>Learned from this task</summary>${cards}<a href="/settings/learning?repo=${encodeURIComponent(view.repo)}">Learning settings</a></details>` : html ``;
563
569
  const history = view.events.map(ev => html `<article class="card" data-learning-event="${ev.action}"><p><strong>${{ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action}</strong>${ev.action === "reuse" ? ` · ${ev.after}` : ""}</p><p class="meta">${whenUtc(ev.at)} · ${ev.actor}${ev.run === null ? '' : html ` · <a href="/r/${ev.run}">Run #${ev.run}</a>${ev.outcome === null ? "" : ` · ${ev.outcome}`}`}</p><details><summary>Details</summary><p>${ev.before} → ${ev.after}</p><p>${ev.reason}</p>${ev.lesson === null ? '' : html `<p>Lesson #${ev.lesson}</p>`}<ul>${evidence(ev.evidence, ev.run)}</ul>${ev.snapshot === null ? "" : html `<details><summary>Exact context</summary><pre>${ev.snapshot}</pre></details>`}</details></article>`);
@@ -96,7 +96,7 @@ export type LeaseResult = {
96
96
  };
97
97
  export type LeaseFailure = "held" | "dirty" | "git" | "unverified" | "unknown-runner" | "in-use";
98
98
  /** Why storage retention left a checkout in place. */
99
- export type KeptWhy = "has changes" | "has commits" | "unreadable" | "git refused";
99
+ export type KeptWhy = "has changes" | "has commits" | "not on its branch" | "unreadable" | "git refused";
100
100
  /**
101
101
  * The note a lease leaves in the checkout, naming the process holding it.
102
102
  *
@@ -291,15 +291,24 @@ export declare class WorktreePool {
291
291
  * or remote. Reads only, through `git` when given (teardown's hardened git).
292
292
  */
293
293
  inspect(path: string, git?: Git): Promise<"clean" | KeptWhy>;
294
+ /**
295
+ * Whether a checkout a task may still come back to can go, beyond `inspect`: a later lease of its branch must make the
296
+ * same checkout, and every commit its task recorded must outlive it (git's garbage collection counts a worktree's HEAD
297
+ * and reflog; once they go only a branch holds a commit). So: the folder is the pool's own, the checkout belongs to its
298
+ * project by the plane's record (`boundDirs`, not its `.git` file), its HEAD is its recorded branch, every recorded
299
+ * `heads` commit is on a branch, and no ref lives only in this worktree. Reads only; anything unreadable keeps it.
300
+ */
301
+ proveRecreatable(row: WorktreeRow, heads: readonly string[]): Promise<"clean" | KeptWhy>;
294
302
  /**
295
303
  * Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
296
304
  * let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
297
305
  * lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
298
306
  * made after that). Only the working copy goes: its branch, and so every commit,
299
307
  * stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
300
- * through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
308
+ * through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts). `proof` is asked last,
309
+ * inside the reservation, for what `inspect` doesn't cover (idle cleanup: `proveRecreatable`).
301
310
  */
302
- prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git): Promise<{
311
+ prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git, proof?: (row: WorktreeRow) => Promise<"clean" | KeptWhy>): Promise<{
303
312
  removed: WorktreeRow[];
304
313
  kept: {
305
314
  path: string;
package/dist/worktree.js CHANGED
@@ -28,6 +28,7 @@ import { hostname } from "node:os";
28
28
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
29
29
  import { run } from "./exec.js";
30
30
  import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
31
+ import { currentMachineId, isThisMachine } from "./machine-identity.js";
31
32
  import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } from "./evidence.js";
32
33
  import { parseHandoff } from "./decision.js";
33
34
  import { diskBytes } from "./storage.js";
@@ -55,7 +56,8 @@ const reservationPath = (path) => `${path}.occupancy`;
55
56
  function reservation(path) {
56
57
  try {
57
58
  const note = JSON.parse(readFileSync(reservationPath(path), "utf8"));
58
- if (Number.isSafeInteger(note.pid) && note.pid > 0 && typeof note.host === "string" && typeof note.token === "string")
59
+ if (Number.isSafeInteger(note.pid) && note.pid > 0 && typeof note.host === "string" && typeof note.token === "string"
60
+ && (note.machineId === undefined || note.machineId === null || typeof note.machineId === "string"))
59
61
  return note;
60
62
  }
61
63
  catch (error) {
@@ -66,9 +68,9 @@ function reservation(path) {
66
68
  return { pid: 0, host: "unknown", bootId: null, token: "unknown" };
67
69
  }
68
70
  function reservationAlive(note) {
69
- if (note.pid === 0 || note.host !== hostname())
71
+ if (note.pid === 0 || !isThisMachine(note))
70
72
  return true;
71
- if (note.bootId !== null && provenDeadByBootChange(note))
73
+ if (note.bootId !== null && provenDeadByBootChange(note, { host: note.host, bootId: currentBootId() }))
72
74
  return false;
73
75
  try {
74
76
  process.kill(note.pid, 0);
@@ -81,7 +83,7 @@ function reservationAlive(note) {
81
83
  /** A short store transaction arbitrates marker creation/recovery, never git, a scan or deletion.
82
84
  * Atomic rename publishes a complete owner; a crash cannot leave a half-written reservation. */
83
85
  function reserveCheckout(store, path) {
84
- const note = { pid: process.pid, host: hostname(), bootId: currentBootId(), token: randomBytes(12).toString("hex") };
86
+ const note = { pid: process.pid, host: hostname(), bootId: currentBootId(), token: randomBytes(12).toString("hex"), machineId: currentMachineId() };
85
87
  const file = reservationPath(path), temp = `${file}.${note.token}`;
86
88
  const held = store.transact(() => {
87
89
  const prior = reservation(path);
@@ -155,6 +157,18 @@ function safePoolPath(root, target) {
155
157
  }
156
158
  return true;
157
159
  }
160
+ /** The checkout itself, strictly inside the pool as real paths, reached by real folders only: never a link out of it. */
161
+ function insidePool(root, path) {
162
+ if (!safePoolPath(root, path))
163
+ return false;
164
+ try {
165
+ const real = realpathSync(path), base = realpathSync(root);
166
+ return real !== base && real.startsWith(base + sep) && lstatSync(path).isDirectory();
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
158
172
  function normalisePath(path) {
159
173
  return path.replace(/\\/g, "/").replace(/\/+$/, "");
160
174
  }
@@ -178,12 +192,12 @@ export function recordWorktreeProcess(store, path, runner, pid, leaseEpoch) {
178
192
  // verified boot change nothing of it survives, and the checkout is free
179
193
  // without a PID probe that a reused pid could answer wrongly.
180
194
  const bootId = currentBootId();
181
- writeFileSync(join(path, MARKER), `${pid} ${runner} group ${bootId ?? "unknown"} ${hostname()}\n`, "utf8");
195
+ writeFileSync(join(path, MARKER), `${pid} ${runner} group ${bootId ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
182
196
  }
183
197
  /** Read-only occupancy witness shared with stop settlement. */
184
198
  export function worktreeProcessOccupancy(path) {
185
199
  const reserved = reservation(path);
186
- if (reserved !== null && (reserved.pid !== process.pid || reserved.host !== hostname()) && reservationAlive(reserved))
200
+ if (reserved !== null && (reserved.pid !== process.pid || !isThisMachine(reserved)) && reservationAlive(reserved))
187
201
  return { held: true, by: reserved.pid };
188
202
  const note = join(path, MARKER);
189
203
  if (!existsSync(note))
@@ -193,7 +207,9 @@ export function worktreeProcessOccupancy(path) {
193
207
  if (!Number.isInteger(pid) || pid <= 0)
194
208
  return { held: false };
195
209
  const noteHost = parts[4];
196
- if (noteHost !== undefined && noteHost !== hostname())
210
+ // The sixth word (v150) is the writer's machine id; "unknown", or a note from before it, is matched by name.
211
+ const noteMachine = parts[5] === undefined || parts[5] === "unknown" ? null : parts[5];
212
+ if (noteHost !== undefined && !isThisMachine({ host: noteHost, machineId: noteMachine }))
197
213
  return { held: true, by: pid };
198
214
  if (pid === process.pid && parts[2] !== "group")
199
215
  return { held: false };
@@ -201,7 +217,7 @@ export function worktreeProcessOccupancy(path) {
201
217
  // that cannot exist any more; a note without a boot id (legacy) or a
202
218
  // boot this host cannot verify keeps the probe below.
203
219
  const noteBoot = parts[3];
204
- if (noteBoot !== undefined && noteHost !== undefined && provenDeadByBootChange({ host: noteHost, bootId: noteBoot }))
220
+ if (noteBoot !== undefined && noteHost !== undefined && provenDeadByBootChange({ host: noteHost, bootId: noteBoot }, { host: noteHost, bootId: currentBootId() }))
205
221
  return { held: false };
206
222
  // A shell/provider can exit before its descendants. A recorded POSIX
207
223
  // process group remains an owner until that whole group has gone.
@@ -650,7 +666,7 @@ export class WorktreePool {
650
666
  /** Leave a note naming the process holding this checkout. */
651
667
  mark(path, runner, pid = process.pid) {
652
668
  try {
653
- writeFileSync(join(path, MARKER), `${pid} ${runner} process ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
669
+ writeFileSync(join(path, MARKER), `${pid} ${runner} process ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
654
670
  return true;
655
671
  }
656
672
  catch {
@@ -846,15 +862,49 @@ export class WorktreePool {
846
862
  return "unreadable";
847
863
  return own.stdout.trim() === "" ? "clean" : "has commits";
848
864
  }
865
+ /**
866
+ * Whether a checkout a task may still come back to can go, beyond `inspect`: a later lease of its branch must make the
867
+ * same checkout, and every commit its task recorded must outlive it (git's garbage collection counts a worktree's HEAD
868
+ * and reflog; once they go only a branch holds a commit). So: the folder is the pool's own, the checkout belongs to its
869
+ * project by the plane's record (`boundDirs`, not its `.git` file), its HEAD is its recorded branch, every recorded
870
+ * `heads` commit is on a branch, and no ref lives only in this worktree. Reads only; anything unreadable keeps it.
871
+ */
872
+ async proveRecreatable(row, heads) {
873
+ if (row.branch === "unknown" || !insidePool(this.options.root, row.path) || heads.some(one => !/^[0-9a-f]{40,64}$/.test(one)))
874
+ return "unreadable";
875
+ const git = teardownGit(this.runner, { repo: row.repo });
876
+ const head = await git(row.path, [...READ_ONLY, "symbolic-ref", "-q", "HEAD"]);
877
+ if (head.code === 1)
878
+ return "not on its branch";
879
+ if (head.code !== 0)
880
+ return "unreadable";
881
+ if (head.stdout.trim() !== `refs/heads/${row.branch}`)
882
+ return "not on its branch";
883
+ const branch = await git(row.repo, [...READ_ONLY, "rev-parse", "--verify", "--quiet", `refs/heads/${row.branch}^{commit}`]);
884
+ if (branch.code !== 0)
885
+ return "unreadable";
886
+ if (heads.length > 0) {
887
+ const unheld = await git(row.repo, [...READ_ONLY, "rev-list", "-1", ...heads, "--not", "--branches"]);
888
+ if (unheld.code !== 0)
889
+ return "unreadable";
890
+ if (unheld.stdout.trim() !== "")
891
+ return "has commits";
892
+ }
893
+ const own = await git(row.path, [...READ_ONLY, "for-each-ref", "--count=1", "refs/worktree", "refs/bisect", "refs/rewritten"]);
894
+ if (own.code !== 0)
895
+ return "unreadable";
896
+ return own.stdout.trim() === "" ? "clean" : "has commits";
897
+ }
849
898
  /**
850
899
  * Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
851
900
  * let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
852
901
  * lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
853
902
  * made after that). Only the working copy goes: its branch, and so every commit,
854
903
  * stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
855
- * through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
904
+ * through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts). `proof` is asked last,
905
+ * inside the reservation, for what `inspect` doesn't cover (idle cleanup: `proveRecreatable`).
856
906
  */
857
- async prune(rows, wanted, max = Infinity, git = this.teardown) {
907
+ async prune(rows, wanted, max = Infinity, git = this.teardown, proof) {
858
908
  const removed = [];
859
909
  const kept = [];
860
910
  const idle = (row, releasedAt) => row !== null && row.runner === null && row.releasedAt === releasedAt && !this.inUse(row.path).held;
@@ -888,6 +938,11 @@ export class WorktreePool {
888
938
  const allowed = wanted(now, true);
889
939
  if (!(typeof allowed === "boolean" ? allowed : await allowed))
890
940
  continue;
941
+ const proven = proof === undefined ? "clean" : await proof(now);
942
+ if (proven !== "clean") {
943
+ kept.push({ path: row.path, why: proven });
944
+ continue;
945
+ }
891
946
  const gone = await this.removeCleanCopy(row, git);
892
947
  if (gone !== "removed") {
893
948
  kept.push({ path: row.path, why: gone });
@@ -931,7 +986,7 @@ export class WorktreePool {
931
986
  return { ok: false, message: `${row.path} is in use` };
932
987
  // Held while it slims: another process's lease sees this process in it.
933
988
  try {
934
- writeFileSync(note, `${process.pid} storage-retention slimming ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
989
+ writeFileSync(note, `${process.pid} storage-retention slimming ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
935
990
  held = true;
936
991
  }
937
992
  catch {
@@ -989,7 +1044,7 @@ export class WorktreePool {
989
1044
  // Held while it goes: another process's lease sees this process in it (the busy map is this process's only).
990
1045
  const note = join(row.path, MARKER);
991
1046
  try {
992
- writeFileSync(note, `${process.pid} storage-retention removing ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
1047
+ writeFileSync(note, `${process.pid} storage-retention removing ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
993
1048
  }
994
1049
  catch {
995
1050
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.92",
3
+ "version": "0.9.94",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -1,63 +0,0 @@
1
- /**
2
- * /next (Wave E, src/design/KIT.md): everything waiting on a person, one at a time, each in the approval slots with
3
- * its one act, every act landing back here. The forms post exactly what they always have (the approve form's nonce,
4
- * digest and password; a decision's choice, confirm and note; a requeue), so every server check is unchanged. The
5
- * approval shows the password only when the rules ask (approveForm, standard): none with Passwordless or a remembered
6
- * password the task page's own preview says may approve these exact terms; otherwise the password, required.
7
- */
8
- import { type TaskOrigin } from "../task-source.js";
9
- import type { Gap } from "../gaps.js";
10
- import type { Scope } from "../scope.js";
11
- import type { Decision } from "../store.js";
12
- import { type Chrome, type Screen } from "./chrome.js";
13
- import { type PlanContractView, type RouteView } from "./render-tasks.js";
14
- export type NextItem = {
15
- key: string;
16
- kind: "decision";
17
- decision: Decision & {
18
- taskId: string;
19
- };
20
- } | {
21
- key: string;
22
- kind: "approval";
23
- approval: {
24
- taskId: string;
25
- title: string;
26
- goal: string;
27
- digest: string;
28
- proposedAt: string;
29
- };
30
- } | {
31
- key: string;
32
- kind: "requeue";
33
- stalled: {
34
- taskId: string;
35
- title: string;
36
- strikes: number;
37
- incidentCount: number;
38
- };
39
- } | {
40
- key: string;
41
- kind: "gap";
42
- gap: Gap;
43
- } | null;
44
- export declare function nextPage(chrome: Chrome, data: {
45
- item: NextItem;
46
- scope: Scope | null;
47
- planDocument: string | null;
48
- planContract?: PlanContractView | null;
49
- approvalDigest: string | null;
50
- /** v34: said inside the ceremony when the yes buys a report, not a branch. */
51
- deliverable?: "branch" | "report";
52
- /** v48: the agents the yes freezes — the one block every ceremony shows. */
53
- route: RouteView | null;
54
- /** v126: where the work came from (an outside event's line of context, never a term agreed to). */
55
- origin?: TaskOrigin | null;
56
- /** A remembered password stands in for these exact terms (the task page's preview): one click, no field. */
57
- remembered?: boolean;
58
- csrf: string;
59
- nonce: string;
60
- remaining: number;
61
- skipped: string[];
62
- now: Date;
63
- }): Screen;
@@ -1,109 +0,0 @@
1
- /**
2
- * /next (Wave E, src/design/KIT.md): everything waiting on a person, one at a time, each in the approval slots with
3
- * its one act, every act landing back here. The forms post exactly what they always have (the approve form's nonce,
4
- * digest and password; a decision's choice, confirm and note; a requeue), so every server check is unchanged. The
5
- * approval shows the password only when the rules ask (approveForm, standard): none with Passwordless or a remembered
6
- * password the task page's own preview says may approve these exact terms; otherwise the password, required.
7
- */
8
- import { originNote } from "../task-source.js";
9
- import { html, joinHtml, postForm } from "../html.js";
10
- import { screen, when } from "./chrome.js";
11
- import { taskHref } from "./http.js";
12
- import { acceptanceCeremonyHtml, agentsCeremonyHtml, consentClosedHtml, consentDoorOf, executionPlanHtml, planContractHtml, profileWords, runtimeDetailsHtml, } from "./render-tasks.js";
13
- import { approvalFactsNow, approvalTerms, approveForm, btn, chip, disclose, doc, docHeader, empty, facts, note, phoneHeader, slots } from "./kit.js";
14
- /** One decision option: a one-click answer (an irreversible one behind its own disclosure, with the confirm the server
15
- * requires) and its consequence. A note travels from the task's own page. */
16
- function optionForm(decision, option) {
17
- const recommended = option.id === decision.recommendation;
18
- const form = postForm(`/d/${decision.id}/answer`, html `<div class="k-acts">${btn(option.label, { variant: option.reversible ? (recommended ? "primary" : "outline") : "danger", type: "submit" })}</div>\
19
- <p class="k-note">${recommended ? html `${chip("Recommended", "", { dot: false })} ` : ""}${option.consequence}</p>`, { attrs: { class: "k-form" }, returnTo: "next", hidden: { choice: option.id, confirm: option.reversible ? null : "yes" } });
20
- return option.reversible ? form : disclose(`${option.label} (can't be undone)`, form);
21
- }
22
- export function nextPage(chrome, data) {
23
- const { item } = data;
24
- const title = "Needs you";
25
- if (item === null) {
26
- const held = data.skipped.length;
27
- return screen(title, html `${phoneHeader(title)}${doc(docHeader({ title, same: true }), held > 0
28
- ? empty({ title: `Only the ${held} you set aside ${held === 1 ? "is" : "are"} left.`, text: "Look at them again now, or come back later.", action: btn("Look at them again", { sm: true, href: "/next" }) })
29
- : empty({ title: "Nothing needs you.", text: "Everything is either working or waiting on its own clock.", action: btn("Open Tasks", { sm: true, href: "/work" }) }))}`, { chrome });
30
- }
31
- const skipHref = `/next?skip=${encodeURIComponent([...data.skipped, item.key].join(","))}`;
32
- const header = docHeader({
33
- title, same: true,
34
- meta: data.remaining === 1 ? "The last thing waiting on you" : `1 of ${data.remaining} waiting on you`,
35
- right: btn("Not now", { variant: "ghost", sm: true, href: skipHref }),
36
- });
37
- const openTask = (taskId, words = "Open the task") => html `<a class="k-link" href="${taskHref(taskId)}">${words}</a>`;
38
- let card;
39
- if (item.kind === "decision") {
40
- const { decision } = item;
41
- card = slots({
42
- wait: true, attrs: { "data-decision": String(decision.id) },
43
- status: html `${chip("Question", "ask")}<h2>${decision.question}</h2>`,
44
- text: decision.recap,
45
- acts: joinHtml(decision.options.map(option => optionForm(decision, option))),
46
- terms: html `${decision.taskId} · asked ${when(decision.createdAt)} · ${openTask(decision.taskId, "Answer with a note on the task")}`,
47
- });
48
- }
49
- else if (item.kind === "approval" && !consentDoorOf(data.scope, data.route).open) {
50
- const door = consentDoorOf(data.scope, data.route);
51
- card = slots({
52
- wait: true,
53
- status: html `${chip("Plan", "ask")}<h2>${item.approval.title}</h2>`,
54
- text: item.approval.taskId,
55
- acts: consentClosedHtml(item.approval.taskId, door, "next"),
56
- terms: openTask(item.approval.taskId),
57
- });
58
- }
59
- else if (item.kind === "approval") {
60
- const scope = data.scope;
61
- const facts_ = approvalFactsNow({ remembered: data.remembered === true });
62
- const terms = joinHtml([
63
- data.deliverable === "report" ? note("Research only: a read-only session investigates this goal and delivers a report. No branch; nothing in the repository changes.") : "",
64
- scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope),
65
- originNote(data.origin) === null ? "" : html `<p class="k-note" data-approval-origin>${originNote(data.origin)}</p>`,
66
- facts([
67
- ["Goal", scope?.goal ?? item.approval.goal],
68
- ["Not this", scope?.outOfScope == null ? "No exclusions" : scope.outOfScope],
69
- ["Touches", scope === null || scope.touches.length === 0 ? "Anything" : scope.touches.join(", ")],
70
- ]),
71
- scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance),
72
- // The agents the yes freezes (v48): the same block the task page and chat sign under, before the password.
73
- agentsCeremonyHtml(data.route),
74
- scope === null ? "" : runtimeDetailsHtml(scope),
75
- ]);
76
- const plan = data.planDocument === null ? "" : html `<section class="k-sec"><h3>The plan</h3>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</section>`;
77
- const digest = data.approvalDigest ?? item.approval.digest;
78
- // The terms ride inside the form, before the password: what the yes covers is on the form that says it.
79
- card = html `${plan}${slots({
80
- wait: true, attrs: { "data-approval": digest },
81
- status: html `${chip("Plan", "ask")}<h2>${item.approval.title}</h2>`,
82
- text: html `${item.approval.taskId} · ${openTask(item.approval.taskId, "Open the task to change the scope first")}`,
83
- acts: approveForm({ action: `${taskHref(item.approval.taskId)}/approve`, kind: "standard", facts: facts_, label: "Approve and start", primary: true,
84
- before: html `<section class="k-sec"><h3>Approve exactly this</h3>${terms}</section>`,
85
- form: { returnTo: "next", hidden: { nonce: data.nonce, digest }, attrs: { class: "k-form" } } }),
86
- terms: approvalTerms("standard", facts_),
87
- })}`;
88
- }
89
- else if (item.kind === "requeue") {
90
- card = slots({
91
- wait: true,
92
- status: html `${chip("Stopped", "ask")}<h2>${item.stalled.title}</h2>`,
93
- text: `Stopped with ${item.stalled.incidentCount} incident${item.stalled.incidentCount === 1 ? "" : "s"}${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt${item.stalled.strikes === 1 ? "" : "s"}` : ""}. Retrying resolves the incidents, clears the failed attempts and puts it back in line.`,
94
- acts: postForm(`${taskHref(item.stalled.taskId)}/requeue`, btn("Retry this work", { variant: "primary", type: "submit" }), { returnTo: "next" }),
95
- terms: html `${item.stalled.taskId} · ${openTask(item.stalled.taskId, "Read the attempts first")}`,
96
- });
97
- }
98
- else {
99
- const { gap } = item;
100
- card = slots({
101
- wait: true,
102
- status: html `${chip("Missing", "ask")}<h2>Supply ${gap.key}</h2>`,
103
- text: html `${gap.state}. Filling it starts ${gap.unblocks.length} task${gap.unblocks.length === 1 ? "" : "s"}: ${joinHtml(gap.unblocks.map(one => html `<span class="k-mono">${one}</span>`), ", ")}.`,
104
- acts: "",
105
- terms: html `Prove it is filled from a terminal: <code class="k-mono">${gap.verify}</code>`,
106
- });
107
- }
108
- return screen(title, html `${phoneHeader(title)}${doc(header, card)}`, { chrome });
109
- }