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
@@ -1,11 +1,12 @@
1
1
  /** Optional repository context. Callers admit project access; this module grants no authority.
2
2
  * The index is disposable. Every read hashes the current bounded tracked source set;
3
3
  * a missing/changed/unreadable index falls back to ordinary text search, never a task gate. */
4
- import { checkoutGitSync } from './teardown-git.js';
4
+ import { checkoutGitSync, viewedGitSync } from './teardown-git.js';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
7
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  import { scanForSecrets } from './evidence.js';
9
+ import { compilerOptionsOf, extractFile, resolveAll } from './code-extract.js';
9
10
  const VERSION = 1;
10
11
  const MAX_FILES = 1200, MAX_FILE_BYTES = 256_000, MAX_SOURCE_BYTES = 12_000_000;
11
12
  const CODE = /\.(?:[cm]?[jt]sx?)$/i;
@@ -15,31 +16,23 @@ const sha = (value) => createHash('sha256').update(value).digest('hex');
15
16
  // Read-only and bounded, and always the hardened checkout git (a crew worktree is agent-written, and a page read or a
16
17
  // lead's refresh reads it alike): no filter, hook, program or network, its config and info frozen in a view of the
17
18
  // plane's own before they're inspected (teardown-git.ts checkoutGitSync).
18
- const git = (repo, args) => checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000 }).trimEnd();
19
+ // Bound to the project's record, so a pointer or commondir can't turn the read to another repository.
20
+ const git = (repo, args, binding) => checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000, binding }).trimEnd();
19
21
  const inside = (root, path) => { const r = relative(root, path); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
20
- function capture(request) {
21
- const repo = realpathSync(request.repo);
22
- if (repo !== resolve(request.repo))
23
- throw Error('The project location changed.');
24
- if (realpathSync(git(repo, ['rev-parse', '--show-toplevel'])) !== repo)
25
- throw Error('Choose the project checkout root.');
26
- const head = git(repo, ['rev-parse', '--verify', 'HEAD']);
27
- const baseRevision = request.baseRevision ?? head;
28
- if (!/^[a-f0-9]{40,64}$/.test(baseRevision) || git(repo, ['rev-parse', '--verify', `${baseRevision}^{commit}`]) !== baseRevision)
29
- throw Error('The requested context base is unavailable.');
30
- if (request.audience === 'crew' && baseRevision !== head)
31
- throw Error('Crew context must start at its exact task base.');
32
- const gitDir = realpathSync(git(repo, ['rev-parse', '--absolute-git-dir']));
33
- if (request.project && realpathSync(git(repo, ['rev-parse', '--path-format=absolute', '--git-common-dir'])) !== realpathSync(git(request.project, ['rev-parse', '--path-format=absolute', '--git-common-dir'])))
34
- throw Error('The worktree belongs to a different project.');
35
- const files = git(repo, ['ls-files', '-z', '--cached']).split('\0').filter(Boolean).sort();
36
- const sources = [], warnings = [];
22
+ /** Whether a tracked path is one the index reads, by its name alone. */
23
+ const skippedPath = (file) => !TEXT.test(file) || /(?:^|\/)(?:node_modules|vendor|dist|build|coverage|\.git)(?:\/|$)/.test(file);
24
+ const unsafePath = (file) => file.split('/').some(p => p === '..') || /[\x00-\x1f]/.test(file);
25
+ const unreadableText = (data, text) => data.length > MAX_FILE_BYTES || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ufffd]/u.test(text) || scanForSecrets(text).length > 0;
26
+ /** The working tree's tracked files, for a lead or a person looking at the project's own checkout. */
27
+ function workingTreeSources(repo, binding) {
28
+ const files = git(repo, ['ls-files', '-z', '--cached'], binding).split('\0').filter(Boolean).sort();
29
+ const sources = [];
37
30
  let omitted = 0, omittedBytes = 0, bytes = 0;
38
31
  for (const file of [...new Set(files)]) {
39
- if (!TEXT.test(file) || /(?:^|\/)(?:node_modules|vendor|dist|build|coverage|\.git)(?:\/|$)/.test(file))
32
+ if (skippedPath(file))
40
33
  continue;
41
34
  const path = resolve(repo, file);
42
- if (!inside(repo, path) || file.split('/').some(p => p === '..') || /[\x00-\x1f]/.test(file)) {
35
+ if (!inside(repo, path) || unsafePath(file)) {
43
36
  omitted++;
44
37
  continue;
45
38
  }
@@ -57,7 +50,7 @@ function capture(request) {
57
50
  }
58
51
  const data = readFileSync(path);
59
52
  const text = data.toString('utf8');
60
- if (data.length > MAX_FILE_BYTES || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ufffd]/u.test(text) || scanForSecrets(text).length) {
53
+ if (unreadableText(data, text)) {
61
54
  omitted++;
62
55
  omittedBytes += data.length;
63
56
  continue;
@@ -69,13 +62,91 @@ function capture(request) {
69
62
  omitted++;
70
63
  }
71
64
  }
72
- if (git(repo, ['rev-parse', '--verify', 'HEAD']) !== head)
65
+ return { sources, omitted, omittedBytes };
66
+ }
67
+ /**
68
+ * A crew's sources: the task base's tree and raw blobs, never the working tree an agent writes. A dirty tracked file,
69
+ * a symlink swapped in or a replace ref is not read; no filter runs on a raw blob.
70
+ */
71
+ function baseCommitSources(repo, base, binding) {
72
+ const read = (args, maxBuffer, input) => {
73
+ const ran = viewedGitSync(repo, ['--no-replace-objects', ...args], { timeoutMs: 10_000, maxBuffer, binding, buffer: true, ...(input === undefined ? {} : { input }) });
74
+ if (ran.status !== 0)
75
+ throw Error('The task base could not be read.');
76
+ return ran.stdout;
77
+ };
78
+ const wanted = [];
79
+ let omitted = 0, omittedBytes = 0, bytes = 0;
80
+ for (const entry of read(['ls-tree', '-r', '-l', '-z', base], 16_000_000).toString('utf8').split('\0')) {
81
+ const match = /^(\d{6}) (\w+) ([0-9a-f]{40,64}) +(\d+|-)\t(.+)$/s.exec(entry);
82
+ if (!match)
83
+ continue;
84
+ const [, mode, kind, oid, sized, file] = match;
85
+ if (skippedPath(file))
86
+ continue;
87
+ const size = sized === '-' ? 0 : Number(sized);
88
+ if (unsafePath(file) || kind !== 'blob' || (mode !== '100644' && mode !== '100755')) {
89
+ omitted++;
90
+ continue;
91
+ }
92
+ if (wanted.length >= MAX_FILES || size > MAX_FILE_BYTES || bytes + size > MAX_SOURCE_BYTES) {
93
+ omitted++;
94
+ omittedBytes += size;
95
+ continue;
96
+ }
97
+ wanted.push({ file, oid });
98
+ bytes += size;
99
+ }
100
+ wanted.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
101
+ const sources = [];
102
+ if (wanted.length === 0)
103
+ return { sources, omitted, omittedBytes };
104
+ const out = read(['cat-file', '--batch'], MAX_SOURCE_BYTES + wanted.length * 128 + 4096, Buffer.from(wanted.map(w => w.oid).join('\n') + '\n'));
105
+ let at = 0;
106
+ for (const { file, oid } of wanted) {
107
+ const end = out.indexOf(0x0a, at);
108
+ const header = end < 0 ? null : /^([0-9a-f]{40,64}) blob (\d+)$/.exec(out.subarray(at, end).toString('latin1'));
109
+ if (end < 0 || header === null || header[1] !== oid)
110
+ throw Error('The task base could not be read.');
111
+ const size = Number(header[2]);
112
+ const data = out.subarray(end + 1, end + 1 + size);
113
+ at = end + 1 + size + 1;
114
+ const text = data.toString('utf8');
115
+ if (unreadableText(data, text)) {
116
+ omitted++;
117
+ omittedBytes += data.length;
118
+ continue;
119
+ }
120
+ sources.push({ file, text, sha256: sha(data) });
121
+ }
122
+ return { sources, omitted, omittedBytes };
123
+ }
124
+ function capture(request) {
125
+ const repo = realpathSync(request.repo);
126
+ if (repo !== resolve(request.repo))
127
+ throw Error('The project location changed.');
128
+ // The project's record, never the checkout's own pointer files: the admitted project, or the one a crew worktree
129
+ // belongs to. Only a person's local look at an unrecorded folder has none.
130
+ const binding = { repo: request.project ?? null };
131
+ if (realpathSync(git(repo, ['rev-parse', '--show-toplevel'], binding)) !== repo)
132
+ throw Error('Choose the project checkout root.');
133
+ const head = git(repo, ['rev-parse', '--verify', 'HEAD'], binding);
134
+ const baseRevision = request.baseRevision ?? head;
135
+ if (!/^[a-f0-9]{40,64}$/.test(baseRevision) || git(repo, ['rev-parse', '--verify', `${baseRevision}^{commit}`], binding) !== baseRevision)
136
+ throw Error('The requested context base is unavailable.');
137
+ if (request.audience === 'crew' && baseRevision !== head)
138
+ throw Error('Crew context must start at its exact task base.');
139
+ const gitDir = realpathSync(git(repo, ['rev-parse', '--absolute-git-dir'], binding));
140
+ const crew = request.audience === 'crew';
141
+ const { sources, omitted, omittedBytes } = crew ? baseCommitSources(repo, baseRevision, binding) : workingTreeSources(repo, binding);
142
+ if (git(repo, ['rev-parse', '--verify', 'HEAD'], binding) !== head)
73
143
  throw Error('The checkout changed while context was being read.');
144
+ const warnings = [];
74
145
  if (omitted)
75
146
  warnings.push(`${omitted} source files were omitted because they were unavailable, excluded, sensitive or beyond the read limits.`);
76
147
  warnings.push('Tracked files only. Static imports do not establish every runtime dependency or sufficient test coverage.');
77
148
  const sourceHash = sha(JSON.stringify(sources.map(({ file, sha256 }) => [file, sha256])));
78
- return { checkout: { repo, gitDir, head, baseRevision, sourceHash, source: 'working-tree' }, sources, omitted, omittedBytes, warnings };
149
+ return { checkout: { repo, gitDir, head, baseRevision, sourceHash, source: crew ? 'base-commit' : 'working-tree' }, sources, omitted, omittedBytes, warnings };
79
150
  }
80
151
  function cacheFile(root, checkout) {
81
152
  const absolute = resolve(root);
@@ -135,62 +206,33 @@ async function buildIndex(snapshot) {
135
206
  dir = dirname(dir);
136
207
  }
137
208
  }
138
- let options = { module: compiler.ModuleKind.NodeNext, moduleResolution: compiler.ModuleResolutionKind.NodeNext, allowJs: true, resolveJsonModule: true };
139
209
  const config = byPath.get(join(snapshot.checkout.repo, 'tsconfig.json'));
210
+ const { options, problem } = compilerOptionsOf(compiler, join(snapshot.checkout.repo, 'tsconfig.json'), config?.text, snapshot.checkout.repo);
140
211
  const warnings = [];
141
- if (config) {
142
- const parsed = compiler.parseConfigFileTextToJson(join(snapshot.checkout.repo, config.file), config.text);
143
- if (!parsed.error && parsed.config?.compilerOptions) {
144
- const converted = compiler.convertCompilerOptionsFromJson(parsed.config.compilerOptions, snapshot.checkout.repo);
145
- options = { ...options, ...converted.options };
146
- if (parsed.config.extends)
147
- warnings.push('Inherited tsconfig settings are not indexed; unresolved aliases remain omitted.');
148
- }
149
- else
150
- warnings.push('TypeScript configuration could not be read; relative imports still work.');
151
- }
212
+ if (problem === 'extends')
213
+ warnings.push('Inherited tsconfig settings are not indexed; unresolved aliases remain omitted.');
214
+ else if (problem === 'unreadable')
215
+ warnings.push('TypeScript configuration could not be read; relative imports still work.');
216
+ const resolveOne = resolveAll(compiler, snapshot.checkout.repo, byPath, directories, options, resolve);
152
217
  const nodes = [], relationships = [];
153
218
  let unresolved = 0, parseFailures = 0;
154
- const host = {
155
- fileExists: file => byPath.has(resolve(file)),
156
- readFile: file => byPath.get(resolve(file))?.text,
157
- directoryExists: dir => directories.has(resolve(dir)),
158
- getCurrentDirectory: () => snapshot.checkout.repo,
159
- realpath: file => resolve(file),
160
- };
161
219
  for (const source of snapshot.sources.filter(s => CODE.test(s.file))) {
162
220
  const fileName = resolve(snapshot.checkout.repo, source.file);
163
- const tree = compiler.createSourceFile(fileName, source.text, compiler.ScriptTarget.Latest, true);
164
- if (tree.parseDiagnostics?.length)
221
+ const read = extractFile(compiler, fileName, source.text);
222
+ if (read.parseFailed)
165
223
  parseFailures++;
166
- const line = (pos) => tree.getLineAndCharacterOfPosition(Math.min(pos, source.text.length)).line + 1;
167
- const relationship = (specifier, kind, at) => {
168
- const resolved = compiler.resolveModuleName(specifier, fileName, options, host).resolvedModule?.resolvedFileName;
169
- if (!resolved || !byPath.has(resolve(resolved))) {
224
+ for (const one of read.declarations)
225
+ if (nodes.length < MAX_NODES)
226
+ nodes.push({ file: source.file, name: one.name, line: one.line, endLine: one.endLine });
227
+ for (const one of read.specifiers) {
228
+ const resolved = resolveOne(one.specifier, fileName);
229
+ if (resolved === null) {
170
230
  unresolved++;
171
- return;
231
+ continue;
172
232
  }
173
233
  if (relationships.length < MAX_EDGES)
174
- relationships.push({ from: source.file, to: byPath.get(resolve(resolved)).file, kind, file: source.file, line: line(at), provenance: 'explicit' });
175
- };
176
- const visit = (node) => {
177
- if (nodes.length < MAX_NODES && (compiler.isFunctionDeclaration(node) || compiler.isClassDeclaration(node) || compiler.isInterfaceDeclaration(node) || compiler.isTypeAliasDeclaration(node) || compiler.isEnumDeclaration(node) || compiler.isMethodDeclaration(node) || compiler.isVariableDeclaration(node))) {
178
- const name = node.name;
179
- if (name && compiler.isIdentifier(name))
180
- nodes.push({ file: source.file, name: name.text, line: line(node.getStart(tree)), endLine: line(node.end) });
181
- }
182
- if (compiler.isImportDeclaration(node) && compiler.isStringLiteral(node.moduleSpecifier))
183
- relationship(node.moduleSpecifier.text, 'imports', node.getStart(tree));
184
- if (compiler.isExportDeclaration(node) && node.moduleSpecifier && compiler.isStringLiteral(node.moduleSpecifier))
185
- relationship(node.moduleSpecifier.text, 're-exports', node.getStart(tree));
186
- if (compiler.isCallExpression(node) && node.arguments.length === 1 && compiler.isStringLiteral(node.arguments[0])) {
187
- if (node.expression.kind === compiler.SyntaxKind.ImportKeyword)
188
- relationship(node.arguments[0].text, 'dynamic-import', node.getStart(tree));
189
- // require may be shadowed; do not present a guessed call as a proven relationship.
190
- }
191
- compiler.forEachChild(node, visit);
192
- };
193
- visit(tree);
234
+ relationships.push({ from: source.file, to: byPath.get(resolved).file, kind: one.kind, file: source.file, line: one.line, provenance: 'explicit' });
235
+ }
194
236
  }
195
237
  if (unresolved)
196
238
  warnings.push(`${unresolved} external or unresolved imports were omitted.`);
@@ -77,6 +77,7 @@ export type RestartBaseline = {
77
77
  bootId: string | null;
78
78
  containment: string | null;
79
79
  open: boolean;
80
+ machineId?: string | null;
80
81
  }[];
81
82
  };
82
83
  limits: readonly string[];
@@ -25,6 +25,7 @@ import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { existsSync, readFileSync, realpathSync, readdirSync, statSync } from "node:fs";
27
27
  import { bootIdentity, provenDeadByBootChange } from "./boot-identity.js";
28
+ import { sameMachineHost } from "./machine-identity.js";
28
29
  import { containmentStatus, currentContainment } from "./containment.js";
29
30
  import { daemonStatus } from "./daemon.js";
30
31
  import { isAlive } from "./runner.js";
@@ -70,8 +71,8 @@ export async function recordRestartBaseline(inputs) {
70
71
  .map(row => ({ run: Number(row["run"]), taskRef: Number(row["task_ref"]), requestedAt: String(row["requested_at"]), problem: store.stopQuiescenceProblem(Number(row["run"])) }));
71
72
  const openRuns = raw.prepare("SELECT id, task_ref, role, runner, lease_id FROM run WHERE outcome IS NULL ORDER BY id").all()
72
73
  .map(row => ({ id: Number(row["id"]), taskRef: Number(row["task_ref"]), role: String(row["role"]), runner: String(row["runner"]), leaseId: String(row["lease_id"]) }));
73
- const witnesses = raw.prepare("SELECT run, pid, host, boot_id, containment, exited_at FROM run_process WHERE exited_at IS NULL ORDER BY id").all()
74
- .map(row => ({ run: Number(row["run"]), pid: row["pid"] === null ? null : Number(row["pid"]), host: String(row["host"]), bootId: row["boot_id"] === null ? null : String(row["boot_id"]), containment: row["containment"] === null ? null : String(row["containment"]), open: true }));
74
+ const witnesses = raw.prepare("SELECT run, pid, host, boot_id, containment, exited_at, machine_id FROM run_process WHERE exited_at IS NULL ORDER BY id").all()
75
+ .map(row => ({ run: Number(row["run"]), pid: row["pid"] === null ? null : Number(row["pid"]), host: String(row["host"]), bootId: row["boot_id"] === null ? null : String(row["boot_id"]), containment: row["containment"] === null ? null : String(row["containment"]), open: true, machineId: row["machine_id"] == null ? null : String(row["machine_id"]) }));
75
76
  return {
76
77
  version: 2,
77
78
  runnerName: inputs.runnerName ?? null,
@@ -123,7 +124,8 @@ export async function verifyRestartRecovery(inputs) {
123
124
  const sameDatabase = baseline.version === 2 && baseline.database.identity === current.database.identity;
124
125
  checks.push({ name: "database", ok: sameDatabase, detail: sameDatabase ? "same database file identity" : "database identity differs or baseline predates identity verification; no recovery mutations performed" });
125
126
  // 1. Boot identity, as the OS reports it.
126
- const sameHost = baseline.host === current.host;
127
+ // This machine under any of its own names (machine-identity.ts): a reboot onto another network can move the hostname.
128
+ const sameHost = sameMachineHost(baseline.host, current.host);
127
129
  const bootChanged = !sameHost || baseline.boot.id === null || current.boot.id === null
128
130
  ? null
129
131
  : provenDeadByBootChange({ host: current.host, bootId: baseline.boot.id }, { host: current.host, bootId: current.boot.id });
@@ -153,7 +155,7 @@ export async function verifyRestartRecovery(inputs) {
153
155
  }
154
156
  const baselineBeats = new Map(baseline.database.runners.map(one => [one.name, one.heartbeatAt]));
155
157
  const liveRunners = current.database.runners.filter(one => !one.retired);
156
- const fresh = liveRunners.filter(one => one.host === current.host && one.alive && (baseline.runnerName === null || one.name === baseline.runnerName) && baselineBeats.has(one.name) && one.heartbeatAt > baselineBeats.get(one.name));
158
+ const fresh = liveRunners.filter(one => sameMachineHost(one.host, current.host) && one.alive && (baseline.runnerName === null || one.name === baseline.runnerName) && baselineBeats.has(one.name) && one.heartbeatAt > baselineBeats.get(one.name));
157
159
  checks.push({
158
160
  name: "heartbeat",
159
161
  ok: fresh.length > 0,
@@ -187,8 +189,8 @@ export async function verifyRestartRecovery(inputs) {
187
189
  ? `every run open at the baseline has ended (${baseline.database.openRuns.map(run => `#${run.id}`).join(", ")})`
188
190
  : `still open from before: ${staleOpen.map(run => `#${run.id} (${run.role}, ${run.runner})`).join(", ")} — recovery has not run, or its lease is still live`,
189
191
  });
190
- const oldWitnesses = baseline.database.witnesses.filter(one => one.bootId !== null && current.boot.id !== null && one.host === current.host);
191
- const survivors = oldWitnesses.filter(one => !provenDeadByBootChange({ host: one.host, bootId: one.bootId }, { host: current.host, bootId: current.boot.id }));
192
+ const oldWitnesses = baseline.database.witnesses.filter(one => one.bootId !== null && current.boot.id !== null && sameMachineHost(one.host, current.host, one.machineId));
193
+ const survivors = oldWitnesses.filter(one => !provenDeadByBootChange({ host: one.host, bootId: one.bootId }, { host: one.host, bootId: current.boot.id }));
192
194
  checks.push({
193
195
  name: "old-boot-custody",
194
196
  ok: bootChanged !== true || survivors.length === 0,
@@ -1,26 +1,25 @@
1
- /** The result page's acts (2026-10-02), chosen in one place: exactly one ink
2
- * act, the one that resolves the result's state, and never navigation; at
3
- * most one outline act beside it. A result that can't be accepted says why
4
- * in one line before them.
1
+ /** The result page's acts (2026-10-02; docs/plans/plain-toolroll.md P1.3), chosen in one place: three verbs, each a
2
+ * relabel over the post it has always made. Exactly one ink act at most, the one that resolves the result's state,
3
+ * and never navigation; at most one outline act beside it. A result that can't be accepted says why in one line
4
+ * before them. Run checks stays in the Checks row, and a person's own checks in the Checks list: neither is an act here.
5
5
  *
6
- * everything met → Accept and finish
7
- * a "You check this one" unanswered → the next check, then Accept without your check
8
- * a check you marked Not right → Request changes, then Accept anyway
9
- * checks queued or running → Checks running (disabled), then Accept without checks
10
- * checks didn't run, a check exists → Run checks, then Accept without checks
6
+ * everything met → Accept
7
+ * a "You check this one" unanswered → (the check itself, above), then Accept without your check
8
+ * a check you marked Not right → Send back, then Accept anyway
9
+ * checks queued or running → (Checks running, in its row), then Accept without checks
10
+ * checks didn't run, a check exists → (Run checks, in its row), then Accept without checks
11
11
  * report and changes disagree,
12
- * or the proof is refuted → Request changes, then Accept (as allowed);
12
+ * or the proof is refuted → Send back, then Accept (as allowed);
13
13
  * never Accept as ink, even with no form here
14
- * a Needs you form resolves it → that form (Build again, Confirm it stopped)
15
- * saved notes or a failing PR → Revise / Draft a repair task
16
- * a failed build → Retry (its note starts with what to change), then Run checks when they
17
- * didn't run; never Accept: a failed build is not a result to accept
14
+ * a Needs you form resolves it → that form (Retry: Build again; Check again)
15
+ * saved notes or a failing PR → Send back (the revision from the notes, or the repair of the PR)
16
+ * a failed build → Retry (its note starts with what to change); never Accept: a failed build
17
+ * is not a result to accept
18
18
  * a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
19
- * accepted; Run checks stays in its Checks row
19
+ * accepted
20
20
  *
21
- * Accepting finishes the task in one request (Accept and finish): the person's
22
- * acceptance, when one is owed, and the completion together. This only decides
23
- * which act is ink; the page recomputes it as the person answers each check. */
21
+ * Accepting finishes the task in one request: the person's acceptance, when one is owed, and the completion
22
+ * together. This only decides which act is ink; the page recomputes it as the person answers each check. */
24
23
  import type { ProofVerdict } from "./proof.js";
25
24
  /** The words over the reason field an Accept of a refuted result requires. */
26
25
  export declare const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
@@ -37,7 +36,7 @@ export declare function evidenceProblemOf(verdict: ProofVerdict | null, reasons:
37
36
  * when nothing refutes it (a person's acceptance settles it). The result page
38
37
  * shows it before Request changes, its one ink act. */
39
38
  export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
40
- export type ResultActKind = "retry" | "accept" | "accept-anyway" | "next-check" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "confirm-stopped" | "revise" | "draft-repair";
39
+ export type ResultActKind = "retry" | "accept" | "accept-anyway" | "next-check" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "recheck-stopped" | "revise" | "draft-repair";
41
40
  export type ResultActFacts = {
42
41
  /** An Accept this page can post (Accept and finish, or the person's acceptance alone); `ready`: every requirement met and checks passed. */
43
42
  accept: {
@@ -52,7 +51,7 @@ export type ResultActFacts = {
52
51
  /** The feedback form is on this page. */
53
52
  canRequest: boolean;
54
53
  /** A Needs you form that resolves it here. */
55
- need: "rebuild" | "confirm-stopped" | null;
54
+ need: "rebuild" | "recheck-stopped" | null;
56
55
  /** Saved notes ready to become a revision, or a failing pull request's repair. */
57
56
  next: "revise" | "draft-repair" | null;
58
57
  /** A failed build: `retry` when the task can be retried from here; `acceptAnyway` when it delivered a result that
@@ -74,12 +73,21 @@ export type ResultActs = {
74
73
  /** The one line before the acts: why it can't be accepted yet. */
75
74
  line: string | null;
76
75
  };
76
+ /** The three verbs, and the posts each one makes (docs/plans/plain-toolroll.md P1.3): Accept is today's Accept (and
77
+ * finish), or Accept anyway with a reason; Send back is today's request for changes and its revision from saved notes
78
+ * for an unpublished result, or the repair of a published one; Retry is today's Retry, or Build again. Check again
79
+ * keeps its own words (its act stands on the build, not on the result). */
80
+ export type ResultVerb = "Accept" | "Send back" | "Retry";
81
+ export declare const RESULT_VERB: Readonly<Record<ResultActKind, ResultVerb | null>>;
82
+ /** What Send back does, said on its own confirm line: the rule is fixed by whether the result is already published. */
83
+ export declare const SEND_BACK_UNPUBLISHED = "Sends it back with your notes. The same task builds again once you approve the revision.";
84
+ export declare const SEND_BACK_PUBLISHED = "It's already published, so this files a fix as a new version for you to approve.";
77
85
  export declare function resultActsOf(facts: ResultActFacts): ResultActs;
78
86
  /** The words a result's Accept wears and the one line before it, given the
79
87
  * server's words for everything but the person's own checks (`base`) and how
80
88
  * those stand on this page: an unanswered check names itself; a check marked
81
89
  * Not right says so; otherwise the base words stand. */
82
- export type AcceptLabel = "Accept and finish" | "Accept without checks" | "Accept without your check" | "Accept anyway";
90
+ export type AcceptLabel = "Accept" | "Accept without checks" | "Accept without your check" | "Accept anyway";
83
91
  export declare function acceptWithChecksOf(base: {
84
92
  label: AcceptLabel;
85
93
  ready: boolean;
@@ -1,26 +1,25 @@
1
- /** The result page's acts (2026-10-02), chosen in one place: exactly one ink
2
- * act, the one that resolves the result's state, and never navigation; at
3
- * most one outline act beside it. A result that can't be accepted says why
4
- * in one line before them.
1
+ /** The result page's acts (2026-10-02; docs/plans/plain-toolroll.md P1.3), chosen in one place: three verbs, each a
2
+ * relabel over the post it has always made. Exactly one ink act at most, the one that resolves the result's state,
3
+ * and never navigation; at most one outline act beside it. A result that can't be accepted says why in one line
4
+ * before them. Run checks stays in the Checks row, and a person's own checks in the Checks list: neither is an act here.
5
5
  *
6
- * everything met → Accept and finish
7
- * a "You check this one" unanswered → the next check, then Accept without your check
8
- * a check you marked Not right → Request changes, then Accept anyway
9
- * checks queued or running → Checks running (disabled), then Accept without checks
10
- * checks didn't run, a check exists → Run checks, then Accept without checks
6
+ * everything met → Accept
7
+ * a "You check this one" unanswered → (the check itself, above), then Accept without your check
8
+ * a check you marked Not right → Send back, then Accept anyway
9
+ * checks queued or running → (Checks running, in its row), then Accept without checks
10
+ * checks didn't run, a check exists → (Run checks, in its row), then Accept without checks
11
11
  * report and changes disagree,
12
- * or the proof is refuted → Request changes, then Accept (as allowed);
12
+ * or the proof is refuted → Send back, then Accept (as allowed);
13
13
  * never Accept as ink, even with no form here
14
- * a Needs you form resolves it → that form (Build again, Confirm it stopped)
15
- * saved notes or a failing PR → Revise / Draft a repair task
16
- * a failed build → Retry (its note starts with what to change), then Run checks when they
17
- * didn't run; never Accept: a failed build is not a result to accept
14
+ * a Needs you form resolves it → that form (Retry: Build again; Check again)
15
+ * saved notes or a failing PR → Send back (the revision from the notes, or the repair of the PR)
16
+ * a failed build → Retry (its note starts with what to change); never Accept: a failed build
17
+ * is not a result to accept
18
18
  * a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
19
- * accepted; Run checks stays in its Checks row
19
+ * accepted
20
20
  *
21
- * Accepting finishes the task in one request (Accept and finish): the person's
22
- * acceptance, when one is owed, and the completion together. This only decides
23
- * which act is ink; the page recomputes it as the person answers each check. */
21
+ * Accepting finishes the task in one request: the person's acceptance, when one is owed, and the completion
22
+ * together. This only decides which act is ink; the page recomputes it as the person answers each check. */
24
23
  /** The words over the reason field an Accept of a refuted result requires. */
25
24
  export const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
26
25
  export const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
@@ -42,34 +41,46 @@ export function cantAcceptYetOf(verdict, reasons, accepted) {
42
41
  ? "Can't accept yet: the project's check failed on these changes."
43
42
  : ACCEPT_NEEDS_REASON;
44
43
  }
44
+ export const RESULT_VERB = {
45
+ accept: "Accept", "accept-anyway": "Accept",
46
+ "request-changes": "Send back", revise: "Send back", "draft-repair": "Send back",
47
+ retry: "Retry", rebuild: "Retry",
48
+ "recheck-stopped": null,
49
+ // Never in the act row: Run checks lives in the Checks row, and a person's own checks in the Checks list.
50
+ "next-check": null, "checks-running": null, "run-checks": null,
51
+ };
52
+ /** What Send back does, said on its own confirm line: the rule is fixed by whether the result is already published. */
53
+ export const SEND_BACK_UNPUBLISHED = "Sends it back with your notes. The same task builds again once you approve the revision.";
54
+ export const SEND_BACK_PUBLISHED = "It's already published, so this files a fix as a new version for you to approve.";
45
55
  export function resultActsOf(facts) {
46
56
  const request = facts.canRequest ? "request-changes" : null;
47
57
  const accept = facts.accept === null ? null : "accept";
48
58
  if (facts.failed != null) {
49
- const checks = facts.runChecks && !facts.checksRunning ? "run-checks" : facts.checksRunning ? "checks-running" : null;
50
59
  const anyway = facts.failed.acceptAnyway === true ? "accept-anyway" : null;
51
- return facts.failed.retry ? { primary: "retry", secondary: anyway ?? checks, line: null } : { primary: checks, secondary: anyway, line: null };
60
+ return facts.failed.retry ? { primary: "retry", secondary: anyway, line: null } : { primary: null, secondary: anyway, line: null };
52
61
  }
53
62
  if (facts.need !== null)
54
63
  return { primary: facts.need, secondary: request, line: null };
64
+ // Saved notes or a failing pull request: Send back is ink, and Accept the quiet other way.
55
65
  if (facts.next !== null)
56
- return { primary: facts.next, secondary: accept ?? request, line: null };
66
+ return { primary: facts.next, secondary: accept, line: null };
57
67
  // Can't be accepted yet: Accept is never the ink act, even when the feedback form isn't here.
58
68
  if (facts.blocked !== null)
59
69
  return { primary: request, secondary: accept, line: facts.blocked };
60
- // The person's own checks come first: the next one is ink until each is answered.
70
+ // The person's own checks come first, in the Checks list above: nothing here is ink until each is answered.
61
71
  if (accept !== null && (facts.unanswered ?? 0) > 0)
62
- return { primary: "next-check", secondary: accept, line: null };
72
+ return { primary: null, secondary: accept, line: null };
63
73
  if (accept !== null && (facts.notRight ?? 0) > 0)
64
74
  return { primary: request, secondary: accept, line: null };
75
+ // Checks running, or not run: their row says so and offers Run checks; Accept waits in outline.
65
76
  if (facts.checksRunning)
66
- return { primary: "checks-running", secondary: accept ?? request, line: null };
77
+ return { primary: null, secondary: accept ?? request, line: null };
67
78
  if (facts.accept === null)
68
79
  return { primary: null, secondary: request, line: null };
69
80
  if (facts.accept.ready)
70
81
  return { primary: "accept", secondary: request, line: null };
71
82
  if (facts.runChecks)
72
- return { primary: "run-checks", secondary: "accept", line: null };
83
+ return { primary: null, secondary: "accept", line: null };
73
84
  return { primary: "accept", secondary: request, line: null };
74
85
  }
75
86
  export function acceptWithChecksOf(base, checks) {
@@ -28,7 +28,7 @@ export type FollowUpCheck = {
28
28
  head: string;
29
29
  actor: string;
30
30
  at: string;
31
- why: "person" | "pull-request" | "batch";
31
+ why: "person" | "pull-request" | "batch" | "recheck";
32
32
  /** The approved command's digest when checks were asked for: only that exact command runs. */
33
33
  digest: string;
34
34
  state: "waiting" | "running" | "passed" | "failed" | "not-run";
@@ -55,7 +55,7 @@ export declare function requestFollowUpChecks(store: Store, input: {
55
55
  runId: number;
56
56
  level: RunLevel;
57
57
  actor: string;
58
- why?: "person" | "pull-request";
58
+ why?: "person" | "pull-request" | "recheck";
59
59
  }, now: Date): {
60
60
  ok: true;
61
61
  request: number;
@@ -1,4 +1,6 @@
1
- import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
1
+ import { widerThan } from "./check-access.js";
2
+ import { recheckStanding } from "./check-only-standing.js";
3
+ import { actualCheckAccess, checkoutLaunchOf, checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
2
4
  import { mkdtempSync, rmSync } from "node:fs";
3
5
  import { tmpdir } from "node:os";
4
6
  import { join } from "node:path";
@@ -72,7 +74,7 @@ export function followUpChecksOf(store, runId, now = new Date(), root) {
72
74
  const started = rows.find(row => row.action === CHECKS_STARTED && row.outcome === String(request.id)) ?? null;
73
75
  const finished = rows.find(row => row.action === CHECKS_FINISHED && row.outcome.startsWith(`${request.id}:`)) ?? null;
74
76
  const base = { request: request.id, runId, level: request.outcome, head, digest, actor: request.actor, at: request.at,
75
- why: why === "pull-request" ? "pull-request" : why === "batch" ? "batch" : "person", exitCode: null, logArtifactId: null, note: null,
77
+ why: why === "pull-request" ? "pull-request" : why === "batch" ? "batch" : why === "recheck" ? "recheck" : "person", exitCode: null, logArtifactId: null, note: null,
76
78
  ...(why === "batch" ? { base: /^[a-f0-9]{40}$/.test(recordedBase) ? recordedBase : null } : {}) };
77
79
  if (finished !== null) {
78
80
  const status = finished.outcome.slice(String(request.id).length + 1);
@@ -200,6 +202,18 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
200
202
  finish("not-run", "The result's commit changed after checks were asked for.");
201
203
  return followUpChecksOf(store, runId, now(), root).find(one => one.request === request) ?? null;
202
204
  }
205
+ // An automatic recheck runs only on its check-only authorization, read again now (check-only.ts): no record, no run.
206
+ const authorized = wanted.why === "recheck" ? recheckStanding(store, { request, runId, head: target.head, digest: wanted.digest }) : null;
207
+ if (wanted.why === "recheck" && !authorized?.ok) {
208
+ finish("not-run", authorized?.problem ?? "Its authorization couldn't be read.");
209
+ return followUpChecksOf(store, runId, now(), root).find(one => one.request === request) ?? null;
210
+ }
211
+ /** Every command of an automatic recheck, refused at the moment it would run wider than the build's own access. */
212
+ const launched = (inner) => !authorized?.ok ? inner : (file, args, options) => {
213
+ if (widerThan(actualCheckAccess(store, checkoutLaunchOf(store, target.repo, [runId]).access), authorized.access))
214
+ return Promise.resolve({ code: 126, stdout: "", stderr: "", notFound: true, timedOut: false });
215
+ return inner(file, args, options);
216
+ };
203
217
  const folder = mkdtempSync(join(deps.scratch ?? tmpdir(), "toolroll-checks-"));
204
218
  const checkout = join(folder, "checkout");
205
219
  const log = [];
@@ -220,7 +234,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
220
234
  let ready = true;
221
235
  if (setup !== null) {
222
236
  const shell = approvedCommandShell(setup.command);
223
- const prepared = await runWithIsolatedDatabase(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level)), shell.file, shell.args, { cwd: checkout, timeoutMs: setup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
237
+ const prepared = await runWithIsolatedDatabase(launched(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level))), shell.file, shell.args, { cwd: checkout, timeoutMs: setup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
224
238
  note("Approved project setup", setup.command, prepared);
225
239
  if (prepared.code !== 0 || prepared.timedOut || prepared.notFound) {
226
240
  ready = false;
@@ -235,7 +249,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
235
249
  }
236
250
  else {
237
251
  const shell = approvedCommandShell(live.command);
238
- const ran = await runWithIsolatedDatabase(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level)), shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
252
+ const ran = await runWithIsolatedDatabase(launched(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level))), shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
239
253
  note(`${CHECK_LEVEL_WORDS[target.level]} check`, live.command, ran);
240
254
  result = ran.timedOut ? { ran: false, failure: "The check ran out of time." } : ran.notFound ? { ran: false, failure: "The check couldn't start." } : { ran: true, exitCode: ran.code };
241
255
  }
@@ -129,7 +129,7 @@ export type SharedResultFacts = {
129
129
  * else from the run record — `headSource` says which. */
130
130
  base: string | null;
131
131
  head: string | null;
132
- headSource: "sealed diff" | "run record" | null;
132
+ headSource: "saved diff" | "build record" | null;
133
133
  /** Signed criteria passed, from the machine's matrix; null = no rubric. */
134
134
  checks: {
135
135
  passed: number;
@@ -0,0 +1,4 @@
1
+ import type { Scope } from "./scope.js";
2
+ import type { Store } from "./store.js";
3
+ /** Whether a line on record for this exact plan describes some other route. An Approve under it is a yes to the wrong words. */
4
+ export declare function routeChangeProblem(store: Store, taskId: string, scope: Scope): boolean;
@@ -0,0 +1,13 @@
1
+ /** The approval's one question about a "what changed" line (route-change.ts), apart so scope.ts loads it without the rest. */
2
+ import { routeDigestOf, routeFromJson } from "./phase-routing.js";
3
+ /** Whether a line on record for this exact plan describes some other route. An Approve under it is a yes to the wrong words. */
4
+ export function routeChangeProblem(store, taskId, scope) {
5
+ const ref = store.lookupRef(taskId);
6
+ if (ref === null)
7
+ return false;
8
+ const row = store.handle.prepare("SELECT to_route_digest FROM route_change WHERE task_ref = ? AND scope_digest = ?").get(ref.id, scope.digest);
9
+ if (row === undefined)
10
+ return false;
11
+ const route = routeFromJson(scope.proposedRouteJson ?? null);
12
+ return route === null || routeDigestOf(route) !== row["to_route_digest"];
13
+ }
@@ -0,0 +1,22 @@
1
+ import { type PhaseRoute } from "./phase-routing.js";
2
+ import { type Scope } from "./scope.js";
3
+ import { type Store } from "./store.js";
4
+ /** The sentence for what changed between the seal and the proposal. */
5
+ export declare function routeChangeWords(sealed: {
6
+ route: PhaseRoute | null;
7
+ profile: Scope["approvedProfile"] | null | undefined;
8
+ authMode?: string | null;
9
+ }, next: {
10
+ route: PhaseRoute;
11
+ authMode?: string | null;
12
+ }): string;
13
+ /** File the same words on today's route, and keep the line. Null: nothing to propose (a release, nothing changed, or no
14
+ * route Toolroll can name), and the task stays as it is. It seals nothing. */
15
+ export declare function proposeNewRoute(store: Store, taskRef: number, taskId: string, now: Date): {
16
+ words: string;
17
+ } | null;
18
+ /** The line for this task's waiting plan, or null. Only while it describes exactly the plan up for approval. */
19
+ export declare function routeChangeOf(store: Store, taskId: string): string | null;
20
+ /** Each sweep: an unapproved row from before routing gets its route, and the same one Approve. A standing old approval
21
+ * is left alone (the stale check reaches it if its route ever differs). */
22
+ export declare function routeLegacyPlans(store: Store, now: Date): number;