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
@@ -5,14 +5,22 @@
5
5
  * 2 days or a week after that, or never (only a clean-up by hand). A clean-up by hand doesn't wait for the setting.
6
6
  * Only the working copy goes: its branch, and so every commit, stays, and a later lease of the branch makes a new one.
7
7
  *
8
- * Always kept: a checkout in use, one whose task isn't finished (on hold, failed, a revision under way), a result
9
- * still waiting for review, a checkout with changes (files Toolroll wrote itself aside), and one with commits on no
10
- * branch. A release check's checkout (a deploy installs from it) goes once its task is complete, with its dependency
8
+ * Kept until idle (below): one whose task isn't finished (on hold, failed, a revision under way) and a result still
9
+ * waiting for review. Always kept: a checkout in use, one with changes (files Toolroll wrote itself aside), and one
10
+ * with commits on no branch. A release check's checkout (a deploy installs from it) goes once its task is complete, with its dependency
11
11
  * install and journey output, except the one deployed now and the project's newest complete one still waiting for its
12
12
  * deploy (no deploy since it was complete; at most a week). A superseded or cancelled gate goes at once, whatever its
13
13
  * task's state (staged releases keep the rollback copy). A checkout no task names (adopted after a crash) goes only
14
14
  * in a clean-up by hand. Every removal is in the ledger.
15
15
  *
16
+ * Idle checkouts go too (the idle limit, 14 days by default): one whose whole task family has been idle that long (no
17
+ * run, no person's act, no open lease) goes even with its result waiting for review or its task unfinished, in the
18
+ * storage sweep's child only, never in a clean-up by hand, and only when `WorktreePool.proveRecreatable` says nothing is lost.
19
+ * Nothing needs that working copy: a result page, Accept, Send back and Run checks read saved evidence and the branch,
20
+ * and a retry, revision or resume leases the branch, which makes a new one. Excluded, because that can't be proven: a
21
+ * release gate (a deploy installs from its checkout), work from an external tracker, and a checkout no task names. A
22
+ * checkout with uncommitted changes is never removed by this; it is listed for a person.
23
+ *
16
24
  * A kept checkout waiting for review or with changes drops its dependencies and build output once its run ends
17
25
  * (`slimKeptCheckouts`); the next run's setup restores them.
18
26
  */
@@ -21,7 +29,8 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
21
29
  import { basename, dirname, join } from "node:path";
22
30
  import { taskBranches } from "./names.js";
23
31
  import { COMPLETION_ACTION } from "./result-completion.js";
24
- import { CLEANUP_CHOICES, bytesWords, diskBytes, diskBytesAsync } from "./storage.js";
32
+ import { CLEANUP_CHOICES, IDLE_CHOICES, bytesWords, diskBytes, diskBytesAsync } from "./storage.js";
33
+ import { checkoutIdle } from "./store.js";
25
34
  import { worktreePath } from "./worktree.js";
26
35
  const DAY_MS = 86_400_000;
27
36
  /** A release candidate's checkout holds the build a deploy installs: the newest complete one waits at most a week for it. */
@@ -218,6 +227,114 @@ export function finishedCheckouts(store, now, repo) {
218
227
  return [{ row, taskId: status.taskId, finishedAt: status.finishedAt }];
219
228
  });
220
229
  }
230
+ /** Toolroll's own bookkeeping about a task: none of it is anyone using the task. Every other ledger entry counts as activity. */
231
+ const NOT_ACTIVITY = ["assignment status baseline", "assignment status observed", "task archived", "checkout removed", "checkout slimmed", "checkout processes stopped",
232
+ "run processes stopped", "orphan process stopped", "process observation failed", "process witness settled", "branch deleted", "landed work cleaned up"];
233
+ /**
234
+ * When each task family last saw a run, a person's act or a lease, read in one pass. A revision builds on what it
235
+ * revises, so a family is idle together. `blocked`: an open run, an unreleased claim, a release gate, or a task only an
236
+ * external tracker knows, in the family.
237
+ */
238
+ export function idleFacts(store) {
239
+ const db = store.handle;
240
+ const refs = db.prepare(`SELECT r.id AS ref, r.external_id AS id, r.revision_of AS revision_of, t.state AS state, t.created_at AS created_at, t.updated_at AS updated_at,
241
+ (SELECT MAX(s.candidate) FROM task_scope s WHERE s.task_id = r.external_id) AS candidate
242
+ FROM task_ref r LEFT JOIN task t ON t.id = r.external_id AND r.backend = 'built-in'`).all();
243
+ const parentOf = new Map(refs.filter(row => row["revision_of"] !== null).map(row => [String(row["id"]), String(row["revision_of"])]));
244
+ const rootOf = (id) => {
245
+ const seen = new Set([id]);
246
+ let root = id;
247
+ for (let parent = parentOf.get(root); parent !== undefined && !seen.has(parent); parent = parentOf.get(root)) {
248
+ seen.add(parent);
249
+ root = parent;
250
+ }
251
+ return root;
252
+ };
253
+ const families = new Map();
254
+ const rootByRef = new Map();
255
+ const rootsById = new Map();
256
+ const note = (root, at, blocked = false) => {
257
+ if (root === undefined)
258
+ return;
259
+ const fact = families.get(root) ?? { activeAt: "", blocked: false };
260
+ if (typeof at === "string" && at > fact.activeAt)
261
+ fact.activeAt = at;
262
+ if (blocked)
263
+ fact.blocked = true;
264
+ families.set(root, fact);
265
+ };
266
+ for (const row of refs) {
267
+ const id = String(row["id"]), root = rootOf(id);
268
+ rootByRef.set(Number(row["ref"]), root);
269
+ rootsById.set(id, root);
270
+ note(root, row["created_at"]);
271
+ note(root, row["updated_at"], row["state"] === null || row["candidate"] !== null);
272
+ }
273
+ const byTaskRef = (at, ref, blocked = false) => note(rootByRef.get(Number(ref)), at, blocked);
274
+ const runRoot = new Map();
275
+ const branches = [];
276
+ for (const row of db.prepare("SELECT id, task_ref, branch, started_at, finished_at FROM run").all()) {
277
+ const root = rootByRef.get(Number(row["task_ref"]));
278
+ runRoot.set(Number(row["id"]), root);
279
+ note(root, row["started_at"], row["finished_at"] === null);
280
+ note(root, row["finished_at"]);
281
+ if (root !== undefined && row["branch"] !== null)
282
+ branches.push([String(row["branch"]), root]);
283
+ }
284
+ for (const row of db.prepare("SELECT task_ref, acquired_at, heartbeat_at, released_at FROM claim").all()) {
285
+ for (const at of [row["acquired_at"], row["heartbeat_at"], row["released_at"]])
286
+ byTaskRef(at, row["task_ref"]);
287
+ byTaskRef(null, row["task_ref"], row["released_at"] === null);
288
+ }
289
+ for (const row of db.prepare("SELECT task_ref, held_at AS at FROM hold UNION ALL SELECT task_ref, created_at FROM task_steer UNION ALL SELECT task_ref, at FROM task_act").all())
290
+ byTaskRef(row["at"], row["task_ref"]);
291
+ for (const row of db.prepare("SELECT run, created_at AS at FROM run_note UNION ALL SELECT run, created_at FROM diff_comment UNION ALL SELECT run, created_at FROM decision UNION ALL SELECT run, answered_at FROM decision").all()) {
292
+ note(runRoot.get(Number(row["run"])), row["at"]);
293
+ }
294
+ for (const row of db.prepare(`SELECT task_id, run_id, MAX(at) AS at FROM action_ledger WHERE (task_id IS NOT NULL OR run_id IS NOT NULL)
295
+ AND action NOT IN (SELECT value FROM json_each(?)) GROUP BY task_id, run_id`).all(JSON.stringify(NOT_ACTIVITY))) {
296
+ if (row["task_id"] !== null)
297
+ note(rootsById.get(String(row["task_id"])), row["at"]);
298
+ if (row["run_id"] !== null)
299
+ note(runRoot.get(Number(row["run_id"])), row["at"]);
300
+ }
301
+ const byRef = new Map();
302
+ for (const [ref, root] of rootByRef)
303
+ byRef.set(ref, families.get(root));
304
+ // A branch answers for every family that used it: the newest activity and any block among them.
305
+ const byBranch = new Map();
306
+ const share = (branch, root) => {
307
+ const one = families.get(root), seen = byBranch.get(branch);
308
+ byBranch.set(branch, seen === undefined ? { ...one } : { activeAt: seen.activeAt > one.activeAt ? seen.activeAt : one.activeAt, blocked: seen.blocked || one.blocked });
309
+ };
310
+ for (const row of refs)
311
+ for (const branch of taskBranches(String(row["id"])))
312
+ share(branch, rootOf(String(row["id"])));
313
+ for (const [branch, root] of branches)
314
+ share(branch, root);
315
+ return { byRef, byBranch };
316
+ }
317
+ /** The idle limit in days as it stands: null when idle cleanup is off (the limit, or automatic cleanup altogether). */
318
+ function idleDays(store) {
319
+ return store.checkoutCleanup() === "never" ? null : IDLE_CHOICES.find(one => one.value === checkoutIdle(store)).days;
320
+ }
321
+ /** When this checkout's task last saw activity, when that is past the limit and only its task's standing keeps it; else null. */
322
+ function idleSince(pool, row, statuses, facts, days, now) {
323
+ if (days === null || row.taskRef === null || row.branch === "unknown" || row.releasedAt === null || row.runner !== null || pool.inUse(row.path).held)
324
+ return null;
325
+ const keep = statusOf(row, statuses)?.keep;
326
+ if (keep !== "waiting for review" && keep !== "task not finished")
327
+ return null;
328
+ const own = facts.byRef.get(row.taskRef), shared = facts.byBranch.get(row.branch);
329
+ if (own === undefined || own.blocked || shared?.blocked === true)
330
+ return null;
331
+ const since = [own.activeAt, shared?.activeAt, row.createdAt, row.leasedAt, row.releasedAt].filter((one) => typeof one === "string").sort().at(-1);
332
+ return Date.parse(since) + days * DAY_MS <= now.getTime() ? since : null;
333
+ }
334
+ /** Every commit a task's runs recorded on this checkout or its branch: what `proveRecreatable` must find on a branch. */
335
+ function recordedHeads(store, row) {
336
+ return store.handle.prepare("SELECT DISTINCT head_revision AS head FROM run WHERE (branch = ? OR worktree = ?) AND head_revision IS NOT NULL").all(row.branch, row.path).map(one => String(one["head"]));
337
+ }
221
338
  /** Why a checkout stays before anyone looks at its files, or null: it may go (as far as its task and the setting say). */
222
339
  function keptFor(pool, row, statuses, now, cleanup) {
223
340
  const status = statusOf(row, statuses);
@@ -266,6 +383,8 @@ export async function checkoutPlan(store, pool, now, options) {
266
383
  const rows = store.listWorktrees().filter(row => existsSync(row.path) && (options.repo === undefined || row.repo === options.repo));
267
384
  const sizes = await diskBytesAsync(rows.map(row => row.path));
268
385
  const statuses = taskStatuses(store, now);
386
+ const days = idleDays(store);
387
+ const facts = idleFacts(store);
269
388
  const items = await eachAtMost(rows, INSPECT_AT_ONCE, async (row) => {
270
389
  const kept = keptFor(pool, row, statuses, now, options.manual ? "manual" : cleanup);
271
390
  let why = kept.why;
@@ -273,14 +392,23 @@ export async function checkoutPlan(store, pool, now, options) {
273
392
  const found = await pool.inspect(row.path);
274
393
  why = found === "clean" ? null : found;
275
394
  }
276
- return { path: row.path, repo: row.repo, branch: row.branch, taskId: kept.taskId, bytes: sizes.get(row.path) ?? 0, releasedAt: row.releasedAt, why, ...(kept.dueAt === undefined ? {} : { dueAt: kept.dueAt }) };
395
+ const since = kept.why === null ? null : idleSince(pool, row, statuses, facts, days, now);
396
+ if (since !== null) {
397
+ const found = await pool.inspect(row.path);
398
+ const proven = found === "clean" ? await pool.proveRecreatable(row, recordedHeads(store, row)) : found;
399
+ why = proven !== "clean" ? proven : options.manual ? "idle" : null;
400
+ }
401
+ return { path: row.path, repo: row.repo, branch: row.branch, taskId: kept.taskId, bytes: sizes.get(row.path) ?? 0, releasedAt: row.releasedAt, why,
402
+ ...(kept.dueAt === undefined ? {} : { dueAt: kept.dueAt }), ...(since === null ? {} : { idleSince: since }) };
277
403
  });
278
404
  const go = items.filter(one => one.why === null).sort((a, b) => b.bytes - a.bytes);
279
405
  const stay = items.filter(one => one.why !== null).sort((a, b) => (a.why ?? "").localeCompare(b.why ?? "") || b.bytes - a.bytes);
406
+ const idle = items.filter(one => one.idleSince !== undefined && (one.why === null || one.why === "idle"));
280
407
  return {
281
- at: now.toISOString(), cleanup, manual: options.manual, go, stay,
408
+ at: now.toISOString(), cleanup, idle: checkoutIdle(store), manual: options.manual, go, stay,
282
409
  count: items.length, totalBytes: items.reduce((sum, one) => sum + one.bytes, 0), freeBytes: go.reduce((sum, one) => sum + one.bytes, 0),
283
410
  waitingReview: stay.filter(one => one.why === "waiting for review").length, withChanges: stay.filter(one => one.why === "has changes").length,
411
+ idleCount: idle.length, idleBytes: idle.reduce((sum, one) => sum + one.bytes, 0),
284
412
  };
285
413
  }
286
414
  /** Names exactly what a preview would remove, so a confirmation removes that and nothing else. */
@@ -290,7 +418,9 @@ export function previewDigest(plan) {
290
418
  /**
291
419
  * Remove what may go: the worker's pass (`manual` false: under the setting, at most AUTO_MAX, one repository) or a
292
420
  * clean-up by hand (no cap, every repository). Each checkout is asked again right before it goes; each removal is in
293
- * the ledger.
421
+ * the ledger. Idle checkouts go only with `idle`, the storage sweep's child (never the service's own pass, whose git
422
+ * and du must not hold up its heartbeats): each is judged again from a fresh read of its task and its family's
423
+ * activity, `spare` (live work), the OS's list of what runs in the folder, and `proveRecreatable`, inside its reservation.
294
424
  */
295
425
  export async function cleanCheckouts(store, pool, now, options) {
296
426
  const cleanup = store.checkoutCleanup();
@@ -299,31 +429,51 @@ export async function cleanCheckouts(store, pool, now, options) {
299
429
  // `only`: the checkouts a person saw in the preview; nothing they didn't see goes.
300
430
  const rows = store.listWorktrees().filter(row => existsSync(row.path) && (options.repo === undefined || row.repo === options.repo) && (options.only === undefined || options.only.has(row.path)));
301
431
  let statuses = taskStatuses(store, now());
432
+ const days = options.idle === undefined ? null : idleDays(store);
433
+ let facts = days === null ? { byRef: new Map(), byBranch: new Map() } : idleFacts(store);
434
+ // The checkouts going for being idle, and since when: judged at every look, so a task coming back drops out.
435
+ const idle = new Map();
436
+ const mayGo = (row) => {
437
+ idle.delete(row.path);
438
+ if (keptFor(pool, row, statuses, now(), options.manual ? "manual" : cleanup).why === null)
439
+ return options.spare?.(row.path) !== true;
440
+ const since = idleSince(pool, row, statuses, facts, days, now());
441
+ if (since === null)
442
+ return false;
443
+ idle.set(row.path, since);
444
+ return options.spare?.(row.path) !== true;
445
+ };
446
+ const refresh = () => { statuses = taskStatuses(store, now()); if (days !== null)
447
+ facts = idleFacts(store); };
302
448
  const sizes = new Map();
303
449
  const wanted = (row, fresh) => {
304
450
  // Right before a checkout goes, where its task stands is read afresh, and its size taken for the ledger.
305
451
  // `spare`: live work uses it (storage-sweep.ts), asked at both looks.
306
452
  if (fresh)
307
- statuses = taskStatuses(store, now());
308
- const go = keptFor(pool, row, statuses, now(), options.manual ? "manual" : cleanup).why === null && options.spare?.(row.path) !== true;
453
+ refresh();
454
+ const go = mayGo(row);
309
455
  if (go && fresh) {
310
456
  sizes.set(row.path, diskBytes([row.path]).get(row.path) ?? 0);
311
457
  // du can be slow. The reservation excludes a concurrent checkout lease, and this last read also catches
312
458
  // an open run/claim or task change that arrived while size was measured in the sweep child.
313
- return keptFor(pool, row, taskStatuses(store, now()), now(), options.manual ? "manual" : cleanup).why === null && options.spare?.(row.path) !== true;
459
+ refresh();
460
+ return mayGo(row) && !(idle.has(row.path) && options.idle?.occupied(row.path) === true);
314
461
  }
315
462
  return go;
316
463
  };
317
- const pruned = await pool.prune(rows, wanted, options.manual ? Infinity : AUTO_MAX);
464
+ const proof = async (row) => idle.has(row.path) ? pool.proveRecreatable(row, recordedHeads(store, row)) : "clean";
465
+ const max = options.manual ? Infinity : AUTO_MAX;
466
+ const pruned = await pool.prune(rows, wanted, max, undefined, proof);
318
467
  const removed = [];
319
468
  for (const row of pruned.removed) {
320
469
  const taskId = row.taskRef === null ? null : store.externalIdFor(row.taskRef);
321
470
  const bytes = sizes.get(row.path) ?? 0;
322
- removed.push({ path: row.path, repo: row.repo, branch: row.branch, taskId, bytes, releasedAt: row.releasedAt, why: null });
471
+ const since = idle.get(row.path);
472
+ removed.push({ path: row.path, repo: row.repo, branch: row.branch, taskId, bytes, releasedAt: row.releasedAt, why: null, ...(since === undefined ? {} : { idleSince: since }) });
323
473
  store.recordAction({ at: now().toISOString(), actor: options.actor, repo: row.repo, taskId, runId: null, action: "checkout removed", outcome: "removed",
324
- source: options.manual ? "request" : "work", detail: `${basename(row.path)} (released ${row.releasedAt?.slice(0, 10) ?? "?"}, about ${bytesWords(bytes)}); branch ${row.branch} kept` });
474
+ source: options.manual ? "request" : "work", detail: `${basename(row.path)} (${since === undefined ? "" : `idle since ${since.slice(0, 10)}, `}released ${row.releasedAt?.slice(0, 10) ?? "?"}, about ${bytesWords(bytes)}); branch ${row.branch} kept` });
325
475
  }
326
- return { removed, kept: pruned.kept, freed: removed.reduce((sum, one) => sum + one.bytes, 0) };
476
+ return { removed, kept: pruned.kept, freed: removed.reduce((sum, one) => sum + one.bytes, 0), ...(pruned.removed.length >= max ? { more: true } : {}) };
327
477
  }
328
478
  /** Checkouts this process already looked at since they were let go (path and release time): looked at once. */
329
479
  const looked = new Set();
@@ -381,6 +531,8 @@ export function whyWords(item) {
381
531
  case "release candidate": return item.dueAt === undefined ? "the deployed release candidate" : `newest release candidate, kept for its deploy until ${item.dueAt.slice(0, 10)}`;
382
532
  case "has changes": return "has changes";
383
533
  case "has commits": return "has commits on no branch";
534
+ case "not on its branch": return "isn't on its task's branch";
535
+ case "idle": return `idle since ${item.idleSince?.slice(0, 10) ?? "a while"}; the next sweep removes it`;
384
536
  case "unreadable": return "git couldn't read it";
385
537
  case "git refused": return "git wouldn't remove it";
386
538
  case "not due yet": return `due ${item.dueAt?.slice(0, 10) ?? "later"}`;
@@ -2,4 +2,7 @@
2
2
  * The top-level usage text, on its own so `toolroll --help` can print it
3
3
  * without loading every command family (see cli-fast.ts).
4
4
  */
5
- export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll at your terminal: start Toolroll here (or attach) and open it in the terminal\n (--web: the browser; --project-root <dir>: where projects live)\n in a repository without a terminal, under an agent or with --json: only says what it would do\n toolroll open [--web] open this computer's Toolroll again, signed in\n toolroll welcome the three steps to a first result (--json: the model)\n toolroll password set add a password (typed twice, at your terminal)\n toolroll scan [path...] report what is in flight (also bare `toolroll` outside a repository)\n toolroll [path...] the same report for the paths given\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / worker release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to your lead: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll doctor check that Toolroll's service holds no key a build could read\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
5
+ export declare const HELP = "toolroll \u2014 hand coding work to agents, walk away, come back to pull requests\n\nUsage\n toolroll at your terminal: start Toolroll here (or attach) and open it in the terminal\n (--web: the browser; --project-root <dir>: where projects live)\n in a repository without a terminal, under an agent or with --json: only says what it would do\n toolroll open [--web] open this computer's Toolroll again, signed in\n toolroll welcome the three steps to a first result (--json: the model)\n toolroll password set add a password (typed twice, at your terminal)\n toolroll scan [path...] report what is in flight (also bare `toolroll` outside a repository)\n toolroll [path...] the same report for the paths given\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll release <branch> release Toolroll itself: checks, your approval, deploy, merge, tag, publish\n (rerun the same command to continue; --help)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's sign-in: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n\nTasks \u2014 `toolroll task` prints the whole list,\nand any task command + --help prints it too\n toolroll approver add <name>\n add a person who can approve work\n toolroll task add <title> queue work\n toolroll task show <id> one task: its plan, builds and what it waits for\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll task accept <id> --finish\n accept the result you read (--despite \"<why>\": accept it with a reason, without finishing)\n toolroll task send-back <id> --feedback \"<what to change>\"\n send the result back for changes\n toolroll task retry <id> build it again\n toolroll task ready <id> can I merge it? each check, and what clears a blocker\n toolroll chat --as <you> --token <t>\n talk to your lead: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n\nMachine and agent commands (the builder, agent tools, recovery):\n toolroll internals lists them; each keeps working as before\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
6
+ /** The commands people rarely type: the builder's own loop, the machine contract and recovery. They all keep working;
7
+ * `toolroll internals` (or `toolroll internals --help`) lists them so top-level help stays about the work. */
8
+ export declare const INTERNALS_HELP = "toolroll internals \u2014 machine and agent commands\n\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll health recent server latency, write waits and live streams\n toolroll ready what could be started right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task scope <id> --goal <text>\n write a task's plan from the command line\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / worker release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll serve --repo <path> run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install run only the builder\n toolroll doctor check that Toolroll's service holds no key a build could read\n toolroll reconcile recover what the last stretch left behind\n toolroll release --recover-deploy [--stage <dir>] [--yes]\n toolroll release --release-gate <id> [--yes]\n the release tool's recovery steps (toolroll release <branch> runs them for you)";
package/dist/cli-help.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * without loading every command family (see cli-fast.ts).
4
4
  */
5
5
  import { DEFAULT_MAX_DEPTH } from "./scan.js";
6
- export const HELP = `toolroll — a control plane for unattended coding agents
6
+ export const HELP = `toolroll — hand coding work to agents, walk away, come back to pull requests
7
7
 
8
8
  Usage
9
9
  toolroll at your terminal: start Toolroll here (or attach) and open it in the terminal
@@ -26,52 +26,41 @@ Usage
26
26
  toolroll unlink take it off again
27
27
  toolroll update update to the latest release: verified, drained, undoable
28
28
  (preview first; --yes, --now, --at HH:MM, --rollback)
29
- toolroll release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish
30
- (resumable: rerun to continue; --help)
31
- toolroll contract the machine contract: envelope version + capabilities
32
- (--commands dumps the declared command guide)
29
+ toolroll release <branch> release Toolroll itself: checks, your approval, deploy, merge, tag, publish
30
+ (rerun the same command to continue; --help)
33
31
  toolroll skills install --claude-code [--dir <path>]
34
32
  install this binary's guides for Claude Code (preview first)
35
33
  toolroll skills install teach a repo's agents this queue exists (preview first)
36
- toolroll skills list the guides this exact binary serves
37
- toolroll skills get <name> print one guide (version-matched, never stale)
38
34
  toolroll demo a seeded throwaway sandbox — see it working in 90 seconds
39
35
  toolroll up app + builder for every saved project — the normal start
40
36
  toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)
41
37
  toolroll status running, queued, ready results, release check and plan windows
42
- toolroll health recent server latency, write waits and live streams
43
38
  toolroll integrations which integrations work, and what to do about the ones that don't (--json)
44
- toolroll lead token mint your lead agent's credential: its own work pings nobody
39
+ toolroll lead token mint your lead agent's sign-in: its own work pings nobody
45
40
  toolroll lead say "<text>" (the lead) one short message in its person's chat
46
41
 
47
- Operating the queue — \`toolroll task\` prints the whole surface,
48
- and any queue command + --help prints it too
42
+ Tasks — \`toolroll task\` prints the whole list,
43
+ and any task command + --help prints it too
49
44
  toolroll approver add <name>
50
- mint the credential that lets a person say yes
51
- toolroll ready what could be dispatched right now
52
- toolroll check-progress <run> current or final approved check progress
53
- toolroll task wait <id> [--timeout <seconds>]
54
- wait for this attempt to finish or need a person
45
+ add a person who can approve work
55
46
  toolroll task add <title> queue work
56
- toolroll task scope <id> --goal <text>
57
- state what success is; approval binds to it
47
+ toolroll task show <id> one task: its plan, builds and what it waits for
58
48
  toolroll task approve <id>
59
49
  the yes — nothing builds without one
60
- toolroll claim <id> --runner <name>
61
- toolroll heartbeat <lease> / worker release <lease> / reap
62
- toolroll tick --runner <name> --token <t> --repo <path>
63
- one unattended pass over the ready set
64
- toolroll flows list | show <id> | create | edit | trigger | script | card | archive
65
- set up and inspect flows (writes preview until --yes)
50
+ toolroll task accept <id> --finish
51
+ accept the result you read (--despite "<why>": accept it with a reason, without finishing)
52
+ toolroll task send-back <id> --feedback "<what to change>"
53
+ send the result back for changes
54
+ toolroll task retry <id> build it again
55
+ toolroll task ready <id> can I merge it? each check, and what clears a blocker
66
56
  toolroll chat --as <you> --token <t>
67
57
  talk to your lead: one conversation across every
68
58
  project; it proposes, you confirm (--say "…" for one turn)
69
- toolroll serve --repo <path> advanced: run only the console
70
- (--editor vscode with --runner: file links
71
- open in VS Code on the device you browse from)
72
- toolroll watch / daemon install advanced: run only the builder
73
- toolroll doctor check that Toolroll's service holds no key a build could read
74
- toolroll reconcile recover what the last stretch left behind
59
+ toolroll flows list | show <id> | create | edit | trigger | script | card | archive
60
+ set up and inspect flows (writes preview until --yes)
61
+
62
+ Machine and agent commands (the builder, agent tools, recovery):
63
+ toolroll internals lists them; each keeps working as before
75
64
 
76
65
  With nothing connected it reports everything it can find below the working
77
66
  directory. Once you connect repositories it reports those instead.
@@ -97,3 +86,31 @@ where listing branches takes milliseconds.
97
86
  link shows what it would do and changes nothing; add --yes to apply it,
98
87
  or --to <dir> to choose where. It never uses sudo, never leaves your
99
88
  home directory, and never touches a file it did not create.`;
89
+ /** The commands people rarely type: the builder's own loop, the machine contract and recovery. They all keep working;
90
+ * `toolroll internals` (or `toolroll internals --help`) lists them so top-level help stays about the work. */
91
+ export const INTERNALS_HELP = `toolroll internals — machine and agent commands
92
+
93
+ toolroll contract the machine contract: envelope version + capabilities
94
+ (--commands dumps the declared command guide)
95
+ toolroll skills list the guides this exact binary serves
96
+ toolroll skills get <name> print one guide (version-matched, never stale)
97
+ toolroll health recent server latency, write waits and live streams
98
+ toolroll ready what could be started right now
99
+ toolroll check-progress <run> current or final approved check progress
100
+ toolroll task wait <id> [--timeout <seconds>]
101
+ wait for this attempt to finish or need a person
102
+ toolroll task scope <id> --goal <text>
103
+ write a task's plan from the command line
104
+ toolroll claim <id> --runner <name>
105
+ toolroll heartbeat <lease> / worker release <lease> / reap
106
+ toolroll tick --runner <name> --token <t> --repo <path>
107
+ one unattended pass over the ready set
108
+ toolroll serve --repo <path> run only the console
109
+ (--editor vscode with --runner: file links
110
+ open in VS Code on the device you browse from)
111
+ toolroll watch / daemon install run only the builder
112
+ toolroll doctor check that Toolroll's service holds no key a build could read
113
+ toolroll reconcile recover what the last stretch left behind
114
+ toolroll release --recover-deploy [--stage <dir>] [--yes]
115
+ toolroll release --release-gate <id> [--yes]
116
+ the release tool's recovery steps (toolroll release <branch> runs them for you)`;
package/dist/cli.js CHANGED
@@ -37,7 +37,8 @@ import { detectGraphs, chooseBackend, setupOptions, LABELS as GRAPH_LABELS, } fr
37
37
  import { openStore, openStoreReadOnly, databasePath } from "./store.js";
38
38
  import { renderReport, renderPulls, renderGraph } from "./render.js";
39
39
  import { DEFAULT_MAX_DEPTH } from "./scan.js";
40
- import { HELP } from "./cli-help.js";
40
+ import { HELP, INTERNALS_HELP } from "./cli-help.js";
41
+ import { withTaskAlias } from "./task-aliases.js";
41
42
  import { quickAnswer } from "./cli-fast.js";
42
43
  import { BIN_NAME, chooseTarget, planLink, applyLink, planUnlink, applyUnlink, } from "./link.js";
43
44
  const USAGE_EXIT = 2;
@@ -113,6 +114,8 @@ export const OPERATE_COMMANDS = new Set([
113
114
  "evals",
114
115
  // Only `lead token` reaches here; the central service's lead verbs answer first.
115
116
  "lead",
117
+ // Only `release approve` reaches here: every other `release` form is release-cli.ts, routed before this set.
118
+ "release",
116
119
  "onboard",
117
120
  "status",
118
121
  "ready",
@@ -135,7 +138,9 @@ export const OPERATE_COMMANDS = new Set([
135
138
  "review",
136
139
  "assignment",
137
140
  "knowledge",
141
+ "map",
138
142
  "memory",
143
+ "flaky",
139
144
  "models",
140
145
  "claim",
141
146
  "heartbeat",
@@ -303,6 +308,8 @@ function routinesMoved(argv, write) {
303
308
  return 0;
304
309
  }
305
310
  async function dispatch(argv, write, mainOptions) {
311
+ // People's names for task commands (task send-back, retry, recheck) are the commands they name, before anything else.
312
+ argv = withTaskAlias(argv);
306
313
  // v115: routines became scheduled flows. Every old `routine …` invocation answers with one line pointing there —
307
314
  // hidden from help and the contract, before anything remote or local opens, changing nothing.
308
315
  if (argv[0] === "routine")
@@ -319,6 +326,11 @@ async function dispatch(argv, write, mainOptions) {
319
326
  // Command families below load when their command runs, so one command never pays for the others.
320
327
  if (argv[0] === 'health')
321
328
  return (await import("./health-cli.js")).runHealthCommand(argv.slice(1), write, mainOptions.connect ?? mainOptions.remote);
329
+ // The machine and agent commands, listed apart so top-level help stays about the work (each still runs by name).
330
+ if (argv[0] === 'internals' && argv.slice(1).every(arg => arg === '--help' || arg === '-h')) {
331
+ write(INTERNALS_HELP);
332
+ return 0;
333
+ }
322
334
  // This computer's own service, read here and never on a central server.
323
335
  if (argv[0] === 'doctor')
324
336
  return (await import("./doctor.js")).runDoctorCommand(argv.slice(1), write);
@@ -344,6 +356,9 @@ async function dispatch(argv, write, mainOptions) {
344
356
  }
345
357
  if (first === "update")
346
358
  return (await import("./toolroll-update-cli.js")).runUpdateCommand(rest, write, mainOptions.update);
359
+ // `toolroll release approve`: a person's approval, with the operate commands' own credential prompt and context.
360
+ if (first === "release" && rest[0] === "approve")
361
+ return runOperate("release", rest, write, mainOptions.operate ?? {});
347
362
  if (first === "release") {
348
363
  const { runReleaseCommand } = await import("./release-cli.js");
349
364
  return runReleaseCommand(rest, write, { releaseLease: lease => runOperate("worker", ["release", ...lease], write, mainOptions.operate ?? {}), ...mainOptions.release });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The static read of one TS/JS file and the module resolution over a set of paths held in memory: nothing here touches
3
+ * the filesystem, a repository or a checker. Repository context (repository-context.ts) reads a checkout through it; the
4
+ * code map's parser child (code-map-main.ts) reads blobs through it. The file name decides the parse mode.
5
+ */
6
+ import type ts from 'typescript-parser';
7
+ export type Compiler = typeof ts;
8
+ export type EdgeKind = 'imports' | 're-exports' | 'dynamic-import';
9
+ export type DeclarationKind = 'function' | 'class' | 'interface' | 'type' | 'enum' | 'method' | 'variable' | 'default';
10
+ export type Declaration = {
11
+ name: string;
12
+ kind: Exclude<DeclarationKind, 'default'>;
13
+ line: number;
14
+ endLine: number;
15
+ };
16
+ export type ExportedName = {
17
+ name: string;
18
+ kind: DeclarationKind;
19
+ line: number;
20
+ };
21
+ export type Specifier = {
22
+ specifier: string;
23
+ kind: EdgeKind;
24
+ line: number;
25
+ };
26
+ export type Extracted = {
27
+ declarations: Declaration[];
28
+ exports: ExportedName[];
29
+ specifiers: Specifier[];
30
+ parseFailed: boolean;
31
+ };
32
+ /** Declarations, the names the file exports (under the name they are exported as) and the module specifiers it names. `require` isn't counted: it may
33
+ * be shadowed, and a guessed call isn't a proven relationship. */
34
+ export declare function extractFile(compiler: Compiler, fileName: string, text: string): Extracted;
35
+ /** The compiler options a project's tsconfig text gives, read as JSONC and converted, non-string options included.
36
+ * `extends` isn't followed. */
37
+ export declare function compilerOptionsOf(compiler: Compiler, configPath: string, text: string | undefined, root: string): {
38
+ options: ts.CompilerOptions;
39
+ problem: 'extends' | 'unreadable' | null;
40
+ };
41
+ /** Resolve a specifier to one of `paths` (absolute, in memory); null when it names nothing there. */
42
+ export declare function resolveAll(compiler: Compiler, root: string, paths: ReadonlyMap<string, {
43
+ text?: string;
44
+ }>, directories: ReadonlySet<string>, options: ts.CompilerOptions, normal: (file: string) => string): (specifier: string, fromFile: string) => string | null;
@@ -0,0 +1,88 @@
1
+ const declarationKind = (compiler, node) => compiler.isFunctionDeclaration(node) ? 'function' : compiler.isClassDeclaration(node) ? 'class' : compiler.isInterfaceDeclaration(node) ? 'interface'
2
+ : compiler.isTypeAliasDeclaration(node) ? 'type' : compiler.isEnumDeclaration(node) ? 'enum' : compiler.isMethodDeclaration(node) ? 'method'
3
+ : compiler.isVariableDeclaration(node) ? 'variable' : null;
4
+ function exportedAtTop(compiler, node) {
5
+ const statement = compiler.isVariableDeclaration(node) ? node.parent?.parent : node;
6
+ if (!statement || !compiler.isSourceFile(statement.parent))
7
+ return false;
8
+ return (compiler.canHaveModifiers(statement) ? compiler.getModifiers(statement) : undefined)?.some(one => one.kind === compiler.SyntaxKind.ExportKeyword) ?? false;
9
+ }
10
+ /** Declarations, the names the file exports (under the name they are exported as) and the module specifiers it names. `require` isn't counted: it may
11
+ * be shadowed, and a guessed call isn't a proven relationship. */
12
+ export function extractFile(compiler, fileName, text) {
13
+ const tree = compiler.createSourceFile(fileName, text, compiler.ScriptTarget.Latest, true);
14
+ const parseFailed = !!tree.parseDiagnostics?.length;
15
+ const line = (pos) => tree.getLineAndCharacterOfPosition(Math.min(pos, text.length)).line + 1;
16
+ const declarations = [], specifiers = [], exports = [];
17
+ const named = [];
18
+ const visit = (node) => {
19
+ const kind = declarationKind(compiler, node);
20
+ if (kind !== null) {
21
+ const name = node.name;
22
+ const at = line(node.getStart(tree));
23
+ const out = exportedAtTop(compiler, node);
24
+ const isDefault = out && (compiler.canHaveModifiers(node) ? compiler.getModifiers(node) : undefined)?.some(one => one.kind === compiler.SyntaxKind.DefaultKeyword);
25
+ if (name && compiler.isIdentifier(name)) {
26
+ declarations.push({ name: name.text, kind, line: at, endLine: line(node.end) });
27
+ if (out)
28
+ exports.push({ name: name.text, kind, line: at });
29
+ }
30
+ else if (!name && isDefault)
31
+ exports.push({ name: 'default', kind: 'default', line: at });
32
+ }
33
+ if (compiler.isExportAssignment(node) && !node.isExportEquals) {
34
+ if (compiler.isIdentifier(node.expression))
35
+ named.push({ local: node.expression.text, as: 'default' });
36
+ else
37
+ exports.push({ name: 'default', kind: 'default', line: line(node.getStart(tree)) });
38
+ }
39
+ if (compiler.isImportDeclaration(node) && compiler.isStringLiteral(node.moduleSpecifier))
40
+ specifiers.push({ specifier: node.moduleSpecifier.text, kind: 'imports', line: line(node.getStart(tree)) });
41
+ if (compiler.isExportDeclaration(node)) {
42
+ if (node.moduleSpecifier && compiler.isStringLiteral(node.moduleSpecifier))
43
+ specifiers.push({ specifier: node.moduleSpecifier.text, kind: 're-exports', line: line(node.getStart(tree)) });
44
+ else if (!node.moduleSpecifier && node.exportClause && compiler.isNamedExports(node.exportClause)) {
45
+ for (const one of node.exportClause.elements)
46
+ named.push({ local: (one.propertyName ?? one.name).text, as: one.name.text });
47
+ }
48
+ }
49
+ if (compiler.isCallExpression(node) && node.arguments.length === 1 && compiler.isStringLiteral(node.arguments[0]) && node.expression.kind === compiler.SyntaxKind.ImportKeyword) {
50
+ specifiers.push({ specifier: node.arguments[0].text, kind: 'dynamic-import', line: line(node.getStart(tree)) });
51
+ }
52
+ compiler.forEachChild(node, visit);
53
+ };
54
+ visit(tree);
55
+ // `export { a as b }` and `export default a`: the local declaration, under the name it is exported as.
56
+ for (const { local, as } of named) {
57
+ const found = declarations.find(one => one.name === local && one.kind !== 'method');
58
+ if (found !== undefined && !exports.some(one => one.name === as))
59
+ exports.push({ name: as, kind: as === 'default' ? 'default' : found.kind, line: found.line });
60
+ }
61
+ return { declarations, exports, specifiers, parseFailed };
62
+ }
63
+ /** The compiler options a project's tsconfig text gives, read as JSONC and converted, non-string options included.
64
+ * `extends` isn't followed. */
65
+ export function compilerOptionsOf(compiler, configPath, text, root) {
66
+ const base = { module: compiler.ModuleKind.NodeNext, moduleResolution: compiler.ModuleResolutionKind.NodeNext, allowJs: true, resolveJsonModule: true };
67
+ if (text === undefined)
68
+ return { options: base, problem: null };
69
+ const parsed = compiler.parseConfigFileTextToJson(configPath, text);
70
+ if (parsed.error || !parsed.config?.compilerOptions)
71
+ return { options: base, problem: 'unreadable' };
72
+ const converted = compiler.convertCompilerOptionsFromJson(parsed.config.compilerOptions, root);
73
+ return { options: { ...base, ...converted.options }, problem: parsed.config.extends ? 'extends' : null };
74
+ }
75
+ /** Resolve a specifier to one of `paths` (absolute, in memory); null when it names nothing there. */
76
+ export function resolveAll(compiler, root, paths, directories, options, normal) {
77
+ const host = {
78
+ fileExists: file => paths.has(normal(file)),
79
+ readFile: file => paths.get(normal(file))?.text,
80
+ directoryExists: dir => directories.has(normal(dir)),
81
+ getCurrentDirectory: () => root,
82
+ realpath: file => normal(file),
83
+ };
84
+ return (specifier, fromFile) => {
85
+ const resolved = compiler.resolveModuleName(specifier, fromFile, options, host).resolvedModule?.resolvedFileName;
86
+ return resolved && paths.has(normal(resolved)) ? normal(resolved) : null;
87
+ };
88
+ }