toolroll 0.9.93 → 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 (328) hide show
  1. package/CHANGELOG.md +68 -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-ui.js +1 -1
  11. package/dist/assignment.js +5 -0
  12. package/dist/boot-identity.d.ts +2 -1
  13. package/dist/boot-identity.js +6 -4
  14. package/dist/bounded-spawn.d.ts +57 -0
  15. package/dist/bounded-spawn.js +220 -0
  16. package/dist/browser/terminal.js +42 -42
  17. package/dist/browser/workspace.css +1 -1
  18. package/dist/browser/workspace.js +32 -32
  19. package/dist/browser-shell.d.ts +1 -1
  20. package/dist/browser-shell.js +2 -2
  21. package/dist/browser-workspace.d.ts +45 -2
  22. package/dist/browser-workspace.js +3 -3
  23. package/dist/builder-restart.d.ts +21 -0
  24. package/dist/builder-restart.js +50 -0
  25. package/dist/builder.d.ts +4 -0
  26. package/dist/builder.js +53 -24
  27. package/dist/chat-acceptance.js +1 -1
  28. package/dist/chat-actions.d.ts +12 -2
  29. package/dist/chat-actions.js +25 -2
  30. package/dist/chat-channel.js +11 -0
  31. package/dist/chat-decide.d.ts +7 -2
  32. package/dist/chat-decide.js +37 -9
  33. package/dist/chat-quiet.js +3 -3
  34. package/dist/chat-voice.d.ts +1 -1
  35. package/dist/chat-voice.js +2 -2
  36. package/dist/check-facts.d.ts +3 -1
  37. package/dist/check-facts.js +57 -11
  38. package/dist/check-failure.d.ts +22 -3
  39. package/dist/check-failure.js +57 -13
  40. package/dist/check-levels-ui.d.ts +4 -0
  41. package/dist/check-levels-ui.js +10 -1
  42. package/dist/check-load.d.ts +85 -0
  43. package/dist/check-load.js +147 -0
  44. package/dist/check-only-standing.d.ts +36 -0
  45. package/dist/check-only-standing.js +39 -0
  46. package/dist/check-only.d.ts +15 -0
  47. package/dist/check-only.js +99 -0
  48. package/dist/check-report.d.ts +2 -0
  49. package/dist/check-report.js +1 -1
  50. package/dist/check-stamp.d.ts +46 -0
  51. package/dist/check-stamp.js +93 -0
  52. package/dist/checkout-cleanup.d.ts +36 -5
  53. package/dist/checkout-cleanup.js +166 -14
  54. package/dist/cli-help.d.ts +4 -1
  55. package/dist/cli-help.js +47 -30
  56. package/dist/cli.js +11 -1
  57. package/dist/code-extract.d.ts +44 -0
  58. package/dist/code-extract.js +88 -0
  59. package/dist/code-map-acts.d.ts +24 -0
  60. package/dist/code-map-acts.js +53 -0
  61. package/dist/code-map-admission.d.ts +52 -0
  62. package/dist/code-map-admission.js +295 -0
  63. package/dist/code-map-build.d.ts +63 -0
  64. package/dist/code-map-build.js +415 -0
  65. package/dist/code-map-cli.d.ts +153 -0
  66. package/dist/code-map-cli.js +103 -0
  67. package/dist/code-map-main.d.ts +1 -0
  68. package/dist/code-map-main.js +48 -0
  69. package/dist/code-map-read.d.ts +106 -0
  70. package/dist/code-map-read.js +171 -0
  71. package/dist/code-map-report.d.ts +37 -0
  72. package/dist/code-map-report.js +95 -0
  73. package/dist/code-map-shape.d.ts +172 -0
  74. package/dist/code-map-shape.js +327 -0
  75. package/dist/code-map-store.d.ts +136 -0
  76. package/dist/code-map-store.js +233 -0
  77. package/dist/coding-provider.d.ts +2 -0
  78. package/dist/coding-provider.js +3 -2
  79. package/dist/coding-update.js +3 -2
  80. package/dist/coding-workspace.js +4 -4
  81. package/dist/console-chrome.js +2 -2
  82. package/dist/containment.d.ts +5 -0
  83. package/dist/containment.js +14 -1
  84. package/dist/contracts/chat-actions.d.ts +9 -0
  85. package/dist/contracts/chat-actions.js +2 -0
  86. package/dist/contracts/cli.d.ts +52 -0
  87. package/dist/contracts/cli.js +4 -0
  88. package/dist/contracts/console-api.d.ts +10 -3
  89. package/dist/contracts/console-api.js +10 -3
  90. package/dist/contracts/gateway-tools.d.ts +17 -0
  91. package/dist/contracts/gateway-tools.js +3 -1
  92. package/dist/contracts/lead-tools.d.ts +61 -0
  93. package/dist/contracts/lead-tools.js +10 -1
  94. package/dist/contracts/project-knowledge.d.ts +38 -5
  95. package/dist/contracts/project-knowledge.js +11 -5
  96. package/dist/contracts/project-memory.d.ts +3 -1
  97. package/dist/contracts/project-memory.js +3 -2
  98. package/dist/contracts/storage.d.ts +14 -0
  99. package/dist/contracts/storage.js +3 -1
  100. package/dist/contracts/store-json.d.ts +2 -0
  101. package/dist/contracts/youiyoux-request.d.ts +1 -1
  102. package/dist/contracts/youiyoux-request.js +1 -1
  103. package/dist/desktop-access.js +6 -2
  104. package/dist/dispatch.js +18 -15
  105. package/dist/dispose.js +8 -2
  106. package/dist/easy-start.js +2 -3
  107. package/dist/evals.js +2 -1
  108. package/dist/fast-start.d.ts +136 -0
  109. package/dist/fast-start.js +431 -0
  110. package/dist/fast-terms.d.ts +9 -0
  111. package/dist/fast-terms.js +23 -0
  112. package/dist/fenced-check-coverage.d.ts +62 -0
  113. package/dist/fenced-check-coverage.js +54 -0
  114. package/dist/first-run.js +1 -1
  115. package/dist/flaky-ci.d.ts +31 -0
  116. package/dist/flaky-ci.js +149 -0
  117. package/dist/flaky-cli.d.ts +60 -0
  118. package/dist/flaky-cli.js +60 -0
  119. package/dist/flaky.d.ts +75 -9
  120. package/dist/flaky.js +255 -0
  121. package/dist/flow-card-task.js +3 -0
  122. package/dist/flow-triggers.d.ts +1 -1
  123. package/dist/flow-triggers.js +1 -1
  124. package/dist/github-import.d.ts +62 -0
  125. package/dist/github-import.js +146 -0
  126. package/dist/guides.js +15 -10
  127. package/dist/invoke.d.ts +5 -1
  128. package/dist/invoke.js +49 -14
  129. package/dist/job-object-helper.ps1 +10 -3
  130. package/dist/keymap.d.ts +41 -0
  131. package/dist/keymap.js +51 -0
  132. package/dist/knowledge-cli.js +1 -1
  133. package/dist/knowledge-ui.d.ts +21 -4
  134. package/dist/knowledge-ui.js +75 -7
  135. package/dist/laptop-background.d.ts +16 -0
  136. package/dist/laptop-background.js +108 -2
  137. package/dist/lead-context.js +1 -1
  138. package/dist/lead-contract.d.ts +1 -1
  139. package/dist/lead-contract.js +2 -2
  140. package/dist/lead-progress.js +3 -0
  141. package/dist/lead-status.d.ts +2 -0
  142. package/dist/lead-status.js +6 -2
  143. package/dist/lead-tools.js +51 -2
  144. package/dist/lead.js +1 -1
  145. package/dist/lesson-notes.d.ts +81 -0
  146. package/dist/lesson-notes.js +336 -0
  147. package/dist/limits-ui.js +1 -1
  148. package/dist/local-owner.d.ts +1 -1
  149. package/dist/local-owner.js +11 -0
  150. package/dist/machine-check.d.ts +16 -0
  151. package/dist/machine-check.js +26 -0
  152. package/dist/machine-identity.d.ts +75 -0
  153. package/dist/machine-identity.js +170 -0
  154. package/dist/mcp-core.js +11 -1
  155. package/dist/memory-cli.d.ts +300 -1
  156. package/dist/memory-cli.js +62 -9
  157. package/dist/memory-pass.d.ts +16 -0
  158. package/dist/memory-pass.js +67 -3
  159. package/dist/memory-tidy.d.ts +181 -0
  160. package/dist/memory-tidy.js +523 -0
  161. package/dist/memory-use.d.ts +35 -0
  162. package/dist/memory-use.js +109 -0
  163. package/dist/merge-checklist.d.ts +55 -0
  164. package/dist/merge-checklist.js +182 -0
  165. package/dist/needs-you.d.ts +27 -8
  166. package/dist/needs-you.js +96 -39
  167. package/dist/nightly-github.d.ts +2 -0
  168. package/dist/nightly-github.js +4 -4
  169. package/dist/operate-remote-arguments.js +6 -1
  170. package/dist/operate-remote.js +3 -1
  171. package/dist/operate.d.ts +6 -1
  172. package/dist/operate.js +402 -52
  173. package/dist/phase-routing.js +2 -2
  174. package/dist/plain-schema.d.ts +23 -0
  175. package/dist/plain-schema.js +94 -0
  176. package/dist/plain-words.d.ts +20 -0
  177. package/dist/plain-words.js +66 -0
  178. package/dist/planner.js +2 -1
  179. package/dist/process-liveness.d.ts +2 -0
  180. package/dist/process-liveness.js +61 -17
  181. package/dist/project-delete-ui.d.ts +7 -2
  182. package/dist/project-delete-ui.js +12 -1
  183. package/dist/project-delete.js +8 -1
  184. package/dist/project-knowledge.d.ts +18 -11
  185. package/dist/project-knowledge.js +39 -9
  186. package/dist/project-learning.d.ts +2 -1
  187. package/dist/project-learning.js +15 -45
  188. package/dist/project-memory.d.ts +5 -0
  189. package/dist/project-memory.js +22 -2
  190. package/dist/proof.js +30 -5
  191. package/dist/provider-auth.js +1 -1
  192. package/dist/publish.js +9 -1
  193. package/dist/pull-request-flow.js +5 -0
  194. package/dist/push.d.ts +18 -2
  195. package/dist/push.js +40 -16
  196. package/dist/release-adapters.d.ts +9 -0
  197. package/dist/release-adapters.js +76 -0
  198. package/dist/release-auto.js +13 -13
  199. package/dist/release-card.d.ts +1 -2
  200. package/dist/release-card.js +11 -0
  201. package/dist/release-carry.d.ts +7 -2
  202. package/dist/release-carry.js +39 -9
  203. package/dist/release-cli.d.ts +4 -0
  204. package/dist/release-cli.js +29 -22
  205. package/dist/release-coverage.js +1 -1
  206. package/dist/release-next.d.ts +48 -0
  207. package/dist/release-next.js +161 -0
  208. package/dist/release.d.ts +20 -1
  209. package/dist/release.js +76 -8
  210. package/dist/remote-destination.d.ts +36 -0
  211. package/dist/remote-destination.js +217 -0
  212. package/dist/remote-main.d.ts +48 -0
  213. package/dist/remote-main.js +84 -5
  214. package/dist/repository-context.d.ts +3 -2
  215. package/dist/repository-context.js +112 -70
  216. package/dist/restart-certification.d.ts +1 -0
  217. package/dist/restart-certification.js +8 -6
  218. package/dist/result-acts.d.ts +27 -19
  219. package/dist/result-acts.js +36 -25
  220. package/dist/result-follow-ups.d.ts +2 -2
  221. package/dist/result-follow-ups.js +18 -4
  222. package/dist/result-review.d.ts +1 -1
  223. package/dist/route-change-check.d.ts +4 -0
  224. package/dist/route-change-check.js +13 -0
  225. package/dist/route-change.d.ts +22 -0
  226. package/dist/route-change.js +89 -0
  227. package/dist/route-hooks.js +3 -2
  228. package/dist/route-results.d.ts +6 -1
  229. package/dist/route-results.js +14 -5
  230. package/dist/run-closeout.d.ts +1 -1
  231. package/dist/run-closeout.js +7 -5
  232. package/dist/runner-process.js +3 -2
  233. package/dist/runner.js +2 -1
  234. package/dist/scope.d.ts +6 -1
  235. package/dist/scope.js +22 -9
  236. package/dist/serve.d.ts +1 -1
  237. package/dist/serve.js +24 -18
  238. package/dist/server/chat.js +14 -12
  239. package/dist/server/chrome.d.ts +1 -1
  240. package/dist/server/chrome.js +26 -35
  241. package/dist/server/console.css +23 -0
  242. package/dist/server/guards.js +3 -0
  243. package/dist/server/home.d.ts +6 -0
  244. package/dist/server/home.js +21 -13
  245. package/dist/server/http.js +1 -1
  246. package/dist/server/kit.js +1 -1
  247. package/dist/server/merge-ready-view.d.ts +10 -0
  248. package/dist/server/merge-ready-view.js +30 -0
  249. package/dist/server/pages-assets.d.ts +1 -0
  250. package/dist/server/pages-assets.js +8 -5
  251. package/dist/server/pages.js +103 -82
  252. package/dist/server/render-chat.js +2 -2
  253. package/dist/server/render-code-map.d.ts +30 -0
  254. package/dist/server/render-code-map.js +137 -0
  255. package/dist/server/render-flaky.d.ts +20 -0
  256. package/dist/server/render-flaky.js +59 -0
  257. package/dist/server/render-history.js +1 -1
  258. package/dist/server/render-new-task.d.ts +26 -1
  259. package/dist/server/render-new-task.js +30 -5
  260. package/dist/server/render-pages.d.ts +1 -175
  261. package/dist/server/render-pages.js +38 -313
  262. package/dist/server/render-projects.d.ts +2 -1
  263. package/dist/server/render-projects.js +9 -7
  264. package/dist/server/render-release.d.ts +2 -0
  265. package/dist/server/render-release.js +3 -3
  266. package/dist/server/render-results.d.ts +2 -0
  267. package/dist/server/render-results.js +67 -67
  268. package/dist/server/render-tasks.d.ts +13 -2
  269. package/dist/server/render-tasks.js +153 -136
  270. package/dist/server/retired-routes.d.ts +17 -0
  271. package/dist/server/retired-routes.js +53 -0
  272. package/dist/server/route-table.d.ts +3 -0
  273. package/dist/server/route-table.js +45 -12
  274. package/dist/server/settings.js +129 -22
  275. package/dist/server/task-views.js +61 -26
  276. package/dist/server/tasks.d.ts +2 -6
  277. package/dist/server/tasks.js +174 -321
  278. package/dist/server/welcome.js +1 -1
  279. package/dist/settings-groups-ui.js +1 -1
  280. package/dist/setup-guide.d.ts +2 -2
  281. package/dist/stop-proof.js +6 -4
  282. package/dist/storage-sweep-process.js +1 -1
  283. package/dist/storage-sweep.d.ts +5 -3
  284. package/dist/storage-sweep.js +30 -9
  285. package/dist/storage-ui.js +28 -17
  286. package/dist/storage.d.ts +27 -2
  287. package/dist/storage.js +39 -2
  288. package/dist/store.d.ts +80 -5
  289. package/dist/store.js +296 -87
  290. package/dist/surface.js +45 -4
  291. package/dist/sweep-quarantine.d.ts +1 -0
  292. package/dist/sweep-quarantine.js +6 -4
  293. package/dist/tamper-scan.d.ts +4 -0
  294. package/dist/tamper-scan.js +90 -0
  295. package/dist/task-aliases.d.ts +5 -0
  296. package/dist/task-aliases.js +9 -0
  297. package/dist/task-control.js +4 -1
  298. package/dist/task-outcome-cli.d.ts +2 -0
  299. package/dist/task-outcome-cli.js +17 -17
  300. package/dist/task-sizing.d.ts +5 -0
  301. package/dist/task-sizing.js +2 -0
  302. package/dist/task-source.d.ts +7 -0
  303. package/dist/task-source.js +14 -0
  304. package/dist/task-status.d.ts +9 -0
  305. package/dist/task-status.js +40 -49
  306. package/dist/teardown-git.d.ts +16 -0
  307. package/dist/teardown-git.js +63 -5
  308. package/dist/telegram-progress.js +5 -3
  309. package/dist/telegram-status.js +6 -4
  310. package/dist/test-infra.d.ts +8 -0
  311. package/dist/test-infra.js +18 -0
  312. package/dist/tidy.js +1 -1
  313. package/dist/toolroll-update.js +17 -5
  314. package/dist/tui-contract.d.ts +24 -0
  315. package/dist/tui-source.js +67 -8
  316. package/dist/tui.js +51 -51
  317. package/dist/vanished-retry.d.ts +30 -0
  318. package/dist/vanished-retry.js +174 -0
  319. package/dist/work-index.d.ts +2 -0
  320. package/dist/work-index.js +76 -62
  321. package/dist/work-summary.js +2 -0
  322. package/dist/workspace-ui.d.ts +4 -2
  323. package/dist/workspace-ui.js +28 -22
  324. package/dist/worktree.d.ts +12 -3
  325. package/dist/worktree.js +68 -13
  326. package/package.json +1 -1
  327. package/dist/server/render-next.d.ts +0 -63
  328. package/dist/server/render-next.js +0 -109
@@ -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;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * One step to approve a new route (docs/plans/plain-toolroll.md P2.5). There are no route classes: an approval stays
3
+ * bound to its exact route, and dispatch runs on no other bytes. When the route a task would dispatch on differs from the
4
+ * sealed one, Toolroll files the same words again on today's route (nothing is sealed) and keeps one "what changed" line,
5
+ * bound to that route's digest, for the one Approve. A legacy row with no route gets the same.
6
+ *
7
+ * approve() reads routeChangeProblem: a line that doesn't describe the route being approved refuses "changed".
8
+ */
9
+ import { providerName } from "./provider-auth.js";
10
+ import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
11
+ import { deployGateScope, releaseCheckScope } from "./scope.js";
12
+ import { BUILT_IN } from "./store.js";
13
+ const ROLE = { plan: "planner", build: "builder", repair: "repair agent", review: "reviewer" };
14
+ const named = (one) => `${providerName(one.provider)} ${one.model ?? "(its default model)"}`;
15
+ const billing = (mode) => mode === "api-key" ? "API key" : mode === "subscription" ? "subscription" : null;
16
+ /** The sentence for what changed between the seal and the proposal. */
17
+ export function routeChangeWords(sealed, next) {
18
+ const changes = [];
19
+ for (const phase of ["build", "plan", "repair", "review"]) {
20
+ const now = legOf(next.route, phase);
21
+ const was = sealed.route !== null ? legOf(sealed.route, phase) : phase === "build" && sealed.profile != null ? { provider: sealed.profile.provider, model: "model" in sealed.profile ? (sealed.profile.model ?? null) : null } : null;
22
+ if (was === null) {
23
+ if (phase === "build")
24
+ changes.push(`The builder is ${named(now)}`);
25
+ continue;
26
+ }
27
+ if (was.provider === now.provider && was.model === now.model)
28
+ continue;
29
+ const line = `changes from ${named(was)} to ${named(now)}`;
30
+ // The repair agent is the builder's usual twin: one change is one sentence.
31
+ if (changes.some(one => one.endsWith(line)))
32
+ continue;
33
+ changes.push(`${changes.length === 0 ? "The" : "the"} ${ROLE[phase]} ${line}`);
34
+ }
35
+ const from = billing(sealed.authMode), to = billing(next.authMode);
36
+ const paid = from !== null && to !== null && from !== to ? ` (${from} → ${to})` : "";
37
+ if (changes.length === 0)
38
+ return "The agents' settings changed since you approved this. Approve to continue.";
39
+ return `${changes.join("; ")}${paid}. Approve to continue.`;
40
+ }
41
+ /** File the same words on today's route, and keep the line. Null: nothing to propose (a release, nothing changed, or no
42
+ * route Toolroll can name), and the task stays as it is. It seals nothing. */
43
+ export function proposeNewRoute(store, taskRef, taskId, now) {
44
+ return store.transact(() => {
45
+ const before = store.getScope(taskId);
46
+ if (before === null || deployGateScope(before) || releaseCheckScope(before))
47
+ return null;
48
+ const sealedRoute = routeFromJson(before.approvedRouteJson ?? null);
49
+ const refiled = store.refileScope(taskId, now);
50
+ if (refiled === null || refiled.profileState === "unresolved")
51
+ return null;
52
+ const route = routeFromJson(refiled.proposedRouteJson ?? null);
53
+ if (route === null)
54
+ return null;
55
+ if (refiled.approvedDigest === refiled.digest)
56
+ return null;
57
+ // How the last attempt billed, against how the new builder would: said when they differ.
58
+ const lastBilled = store.runsFor(taskRef).find(one => one.role === "builder")?.authMode ?? null;
59
+ const billed = store.agentsFor([legOf(route, "build").provider])[0]?.billing ?? null;
60
+ const words = routeChangeWords({ route: sealedRoute, profile: before.approvedProfile ?? null, authMode: lastBilled }, { route, authMode: billed });
61
+ store.handle.prepare(`INSERT INTO route_change (task_ref, scope_digest, to_route_digest, words, created_at) VALUES (?, ?, ?, ?, ?)
62
+ ON CONFLICT (task_ref) DO UPDATE SET scope_digest = excluded.scope_digest, to_route_digest = excluded.to_route_digest, words = excluded.words, created_at = excluded.created_at`)
63
+ .run(taskRef, refiled.digest, routeDigestOf(route), words, now.toISOString());
64
+ return { words };
65
+ });
66
+ }
67
+ /** The line for this task's waiting plan, or null. Only while it describes exactly the plan up for approval. */
68
+ export function routeChangeOf(store, taskId) {
69
+ const ref = store.lookupRef(taskId);
70
+ if (ref === null)
71
+ return null;
72
+ const row = store.handle.prepare("SELECT scope_digest, to_route_digest, words FROM route_change WHERE task_ref = ?").get(ref.id);
73
+ const scope = store.getScope(taskId);
74
+ if (row === undefined || scope === null || scope.digest !== row["scope_digest"] || scope.approvedDigest === scope.digest)
75
+ return null;
76
+ const route = routeFromJson(scope.proposedRouteJson ?? null);
77
+ return route !== null && routeDigestOf(route) === row["to_route_digest"] ? String(row["words"]) : null;
78
+ }
79
+ /** Each sweep: an unapproved row from before routing gets its route, and the same one Approve. A standing old approval
80
+ * is left alone (the stale check reaches it if its route ever differs). */
81
+ export function routeLegacyPlans(store, now) {
82
+ const rows = store.handle.prepare(`SELECT r.id AS ref, r.external_id AS task FROM task_scope s JOIN task_ref r ON r.external_id = s.task_id AND r.backend = ? JOIN task t ON t.id = r.external_id
83
+ WHERE s.route_era IS NULL AND t.state = 'queued' AND (s.approved_digest IS NULL OR s.approved_digest <> s.digest) LIMIT 20`).all(BUILT_IN);
84
+ let routed = 0;
85
+ for (const row of rows)
86
+ if (proposeNewRoute(store, Number(row["ref"]), String(row["task"]), now) !== null)
87
+ routed++;
88
+ return routed;
89
+ }
@@ -80,8 +80,9 @@ const VERB = String.raw `(?:INSERT(?:\s+OR\s+\w+)?\s+INTO|REPLACE\s+INTO|UPDATE(
80
80
  const writes = (tables) => new RegExp(String.raw `\b${VERB}\s+(?:main\s*\.\s*)?["\`\[]?(?:${tables.join("|")})["\`\]]?(?![\w])`, "i");
81
81
  const ROUTING_WRITE = writes([...ROUTING_TABLES, "route_rule_one_winner"]);
82
82
  const MODELS_WRITE = writes(MODELS_TABLES);
83
- /** The two columns only route-store.ts writes: any write statement that names one. */
84
- const COLUMN_WRITE = /\b(?:INSERT|REPLACE|UPDATE)\b[^]*\b(?:exec_class|revision_origin)\b/i;
83
+ /** The two columns only route-store.ts writes: any write statement that names one. A lesson note keeps its own copy of
84
+ * how a revision was asked for: that one INSERT into its own table is not this column, and only it is left out. */
85
+ const COLUMN_WRITE = /\b(?:INSERT(?!\s+INTO\s+lesson_note\b)|REPLACE|UPDATE)\b[^]*\b(?:exec_class|revision_origin)\b/i;
85
86
  /** Each string or template text in the file that writes a protected row, with the function it's in ("" at the top
86
87
  * level): `Class#method`, or the top-level function. */
87
88
  export function protectedWrites(file, source) {
@@ -1,5 +1,5 @@
1
1
  import { INELIGIBLE_REASONS, type RecordReason, type RecordState, type ResultRecordView, type Selection } from "./contracts/route-results.js";
2
- import { type Store } from "./store.js";
2
+ import { type Run, type Store } from "./store.js";
3
3
  export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf, type CohortInput, type CohortRow } from "./route-store.js";
4
4
  /** How long an accepted result waits before it counts, and how long a result may wait for an answer. */
5
5
  export declare const MATURE_AFTER_MS: number;
@@ -39,6 +39,11 @@ export type RecordValue = {
39
39
  /** The family is quiet, or past ACCUMULATE_FOR_MS: with a final verdict, the record closes. */
40
40
  accumulationEnded: boolean;
41
41
  };
42
+ /** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
43
+ * this too, so the two never disagree on what a first check is. */
44
+ export declare function firstCheckPassedOf(store: Store, runs: readonly Pick<Run, "id" | "outcome">[]): boolean | null;
45
+ /** How many of these task versions are a person's revision. */
46
+ export declare function personRevisionsOf(store: Store, refs: readonly number[]): number;
42
47
  /**
43
48
  * A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
44
49
  * proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
@@ -54,6 +54,17 @@ function classOfRun(run, kept) {
54
54
  return "model";
55
55
  }
56
56
  const FAILED_OUTCOMES = new Set(["failed", "interrupted"]);
57
+ /** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
58
+ * this too, so the two never disagree on what a first check is. */
59
+ export function firstCheckPassedOf(store, runs) {
60
+ const first = runs.find(run => run.outcome === "built");
61
+ const status = first === undefined ? null : store.runCheckFor(first.id)?.status ?? null;
62
+ return status === "passed" ? true : status === "failed" ? false : null;
63
+ }
64
+ /** How many of these task versions are a person's revision. */
65
+ export function personRevisionsOf(store, refs) {
66
+ return Number(store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'").get(JSON.stringify(refs))?.["n"] ?? 0);
67
+ }
57
68
  /**
58
69
  * A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
59
70
  * proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
@@ -106,10 +117,8 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
106
117
  else
107
118
  agentTurns += run.agentTurns;
108
119
  }
109
- const firstBuilt = runs.find(run => run.outcome === "built");
110
- const firstCheck = firstBuilt === undefined ? null : store.runCheckFor(firstBuilt.id)?.status ?? null;
111
- const personRevisions = store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'")
112
- .get(JSON.stringify(family.versions.filter(one => one.id !== family.root.id).map(one => one.refId)))?.["n"];
120
+ const firstCheck = firstCheckPassedOf(store, runs);
121
+ const personRevisions = personRevisionsOf(store, family.versions.filter(one => one.id !== family.root.id).map(one => one.refId));
113
122
  const quiet = family.versions.every(one => one.state === "done" || one.state === "cancelled" || one.state === "failed") && runs.every(run => run.outcome !== null);
114
123
  let modelFailures = 0, infraFailures = 0;
115
124
  for (const run of runs) {
@@ -129,7 +138,7 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
129
138
  firstStartedAt: first?.startedAt ?? runs[0].startedAt, acceptedAt: null, decidedAt: null, modelFailures, infraFailures,
130
139
  apiMicrousd, subscriptionRuns, subscriptionTokens, unknownRuns, remediationRuns,
131
140
  selections: attributed.map(one => one.selection).filter((one) => one !== null), settled: false,
132
- personRevisions: Number(personRevisions ?? 0), firstCheckPassed: firstCheck === "passed" ? true : firstCheck === "failed" ? false : null,
141
+ personRevisions, firstCheckPassed: firstCheck,
133
142
  agentTurns, turnsUnknownRuns,
134
143
  accumulationEnded: quiet || now.getTime() >= Date.parse(first?.startedAt ?? runs[0].startedAt) + ACCUMULATE_FOR_MS,
135
144
  };
@@ -1,4 +1,4 @@
1
- import type { Store, WorktreeRow } from "./store.js";
1
+ import { type Store, type WorktreeRow } from "./store.js";
2
2
  /** How long a process has after SIGTERM before SIGKILL. */
3
3
  export declare const STOP_GRACE_MS = 10000;
4
4
  export type ProcessRow = {
@@ -15,9 +15,9 @@
15
15
  */
16
16
  import { execFileSync } from "node:child_process";
17
17
  import { existsSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
18
- import { hostname } from "node:os";
19
18
  import { basename, sep } from "node:path";
20
19
  import { ownedProcessCount, runOwnerTag } from "./exec.js";
20
+ import { localWitnessSql } from "./store.js";
21
21
  import { sleep as defaultSleep } from "./sleep.js";
22
22
  /** How long a process has after SIGTERM before SIGKILL. */
23
23
  export const STOP_GRACE_MS = 10_000;
@@ -138,10 +138,11 @@ export async function closeOutRuns(store, clock, options = {}) {
138
138
  if (process.platform === "win32")
139
139
  return [];
140
140
  const db = store.handle;
141
+ const local = localWitnessSql(db, "p"), mine = localWitnessSql(db);
141
142
  const runs = db.prepare(`SELECT DISTINCT p.run AS run FROM run_process p JOIN run r ON r.id = p.run JOIN task_ref t ON t.id = r.task_ref
142
- WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND p.host = ?
143
+ WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND ${local.sql}
143
144
  AND r.outcome IS NOT NULL${options.repo === undefined ? "" : " AND t.repo = ?"} ORDER BY p.run`)
144
- .all(...[hostname(), ...(options.repo === undefined ? [] : [options.repo])]).map(row => Number(row["run"]));
145
+ .all(...[...local.params, ...(options.repo === undefined ? [] : [options.repo])]).map(row => Number(row["run"]));
145
146
  const out = [];
146
147
  if (runs.length === 0)
147
148
  return out;
@@ -151,7 +152,7 @@ export async function closeOutRuns(store, clock, options = {}) {
151
152
  if (ownedProcessCount(runOwnerTag(store, runId)) > 0)
152
153
  continue;
153
154
  const witnesses = db.prepare(`SELECT pid, process_group, observed_at FROM run_process
154
- WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND host = ?`).all(runId, hostname())
155
+ WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND ${mine.sql}`).all(runId, ...mine.params)
155
156
  .map(row => ({ pid: Number(row["pid"]), group: Number(row["process_group"]) === 1, observedAt: Date.parse(String(row["observed_at"])) }))
156
157
  .filter(one => Number.isFinite(one.observedAt));
157
158
  if (witnesses.length === 0)
@@ -252,8 +253,9 @@ export function checkoutTargets(census, inside, roots, guard) {
252
253
  function finishedRunProcesses(store, census) {
253
254
  const byPid = new Map(census.map(row => [row.pid, row]));
254
255
  const roots = new Set();
256
+ const local = localWitnessSql(store.handle, "p");
255
257
  const rows = store.handle.prepare(`SELECT p.pid AS pid, p.observed_at AS observed_at FROM run_process p JOIN run r ON r.id = p.run
256
- WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.host = ? AND r.outcome IS NOT NULL`).all(hostname());
258
+ WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND ${local.sql} AND r.outcome IS NOT NULL`).all(...local.params);
257
259
  for (const row of rows) {
258
260
  const now = byPid.get(Number(row["pid"]));
259
261
  const observedAt = Date.parse(String(row["observed_at"]));
@@ -13,6 +13,7 @@
13
13
  * The record is the plane's own state (the folder that holds the database). It grants nothing by itself: it can
14
14
  * only let a name whose every recorded holder has exited be taken over early.
15
15
  */
16
+ import { sameMachineHost } from "./machine-identity.js";
16
17
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
17
18
  import { execFileSync } from "node:child_process";
18
19
  import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, rmSync, writeFileSync, fsyncSync, constants } from "node:fs";
@@ -195,7 +196,7 @@ export function holderProvedGone(record, holder, host, probe = realProcessProbe)
195
196
  if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
196
197
  || !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
197
198
  return { gone: false, why: "its process record is not authenticated" };
198
- if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host !== host)
199
+ if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
199
200
  return { gone: false, why: "its record is for another worker or machine" };
200
201
  if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
201
202
  return { gone: false, why: "its record is from an earlier start" };
@@ -229,7 +230,7 @@ export function recordStandsFor(record, holder, host) {
229
230
  if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
230
231
  || !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
231
232
  return "its process record is not authenticated";
232
- if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host !== host)
233
+ if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
233
234
  return "its record is for another worker or machine";
234
235
  if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
235
236
  return "its record is from an earlier start";
package/dist/runner.js CHANGED
@@ -380,7 +380,8 @@ export function recoverDead(store, now, livenessMs = DEFAULT_LIVENESS_MS) {
380
380
  const claims = store.releaseClaimsOf(candidate.name, now, requeued);
381
381
  const worktrees = store.releaseWorktreesOf(candidate.name, now);
382
382
  const settled = store.recoverRunnerWork(candidate.name, now);
383
- return { runner: candidate.name, claims, worktrees, runs: settled.runs, requeued: [...new Set([...requeued, ...settled.requeued])] };
383
+ // Only what recovery left in the queue: a vanished build it stopped for a person isn't requeued (vanished-retry.ts).
384
+ return { runner: candidate.name, claims, worktrees, runs: settled.runs, requeued: [...new Set([...requeued, ...settled.requeued])].filter(id => store.getTask(id)?.state === "queued") };
384
385
  });
385
386
  if (recovery !== null)
386
387
  recovered.push(recovery);
package/dist/scope.d.ts CHANGED
@@ -793,7 +793,12 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
793
793
  reason: "no-mode" | "not-signer" | "not-covered" | "coordinator-filed" | "acceptance-required" | "profile-unresolved";
794
794
  detail?: string;
795
795
  };
796
- export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation): ApproveResult;
796
+ export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation,
797
+ /** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
798
+ fastStart?: {
799
+ rule: string;
800
+ at: string;
801
+ }): ApproveResult;
797
802
  /** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
798
803
  export declare const APPROVAL_PROOF = "approval proved";
799
804
  /** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
package/dist/scope.js CHANGED
@@ -2,6 +2,8 @@
2
2
  export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
3
3
  import { mintSecret } from "./secret-mint.js";
4
4
  import { passwordlessOn } from "./passwordless.js";
5
+ import { routeChangeProblem } from "./route-change-check.js";
6
+ import { issueImported } from "./task-source.js";
5
7
  import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
6
8
  import { validateScopeText } from "./task-text.js";
7
9
  import { contractChangesOf } from "./contract-changes.js";
@@ -1070,7 +1072,9 @@ export function fileAndSealUnderMode(store, input) {
1070
1072
  return { ok: true, scope: store.getScope(input.taskId), basis: "mode" };
1071
1073
  });
1072
1074
  }
1073
- export function approve(store, taskId, by, now, sawDigest, token, mutation = {}) {
1075
+ export function approve(store, taskId, by, now, sawDigest, token, mutation = {},
1076
+ /** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
1077
+ fastStart) {
1074
1078
  // Read and write in one transaction, re-reading inside it — the credential
1075
1079
  // check included, so a token rotated between authentication and the write
1076
1080
  // cannot leave an approval signed by an authority that no longer exists.
@@ -1089,7 +1093,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
1089
1093
  // localApprovalEligible, read again here inside the seal's transaction).
1090
1094
  const local = authenticated.method === "local-owner" ? currentLocalOwner() : null;
1091
1095
  if (authenticated.method === "local-owner") {
1092
- if (local === null || local.act !== "approve-plan")
1096
+ if (local === null || (local.act !== "approve-plan" && !(local.act === "build-now" && fastStart !== undefined)))
1093
1097
  return { ok: false, reason: "policy", message: localOwnerRefusal(store.accountOf(by)?.passwordSet === true, "/").message };
1094
1098
  const eligible = localApprovalEligible(store, by, taskId);
1095
1099
  if (!eligible.ok)
@@ -1106,6 +1110,12 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
1106
1110
  // the new one in silence — which is the whole failure this guards.
1107
1111
  if (sawDigest !== scope.digest)
1108
1112
  return { ok: false, reason: "changed" };
1113
+ // The "what changed" line a person read describes this exact route, or this yes isn't to what they read (route-change.ts).
1114
+ if (routeChangeProblem(store, taskId, scope))
1115
+ return { ok: false, reason: "changed" };
1116
+ // A fast start is never a task from outside (github-import.ts): that takes the ordinary ceremony.
1117
+ if (fastStart !== undefined && issueImported(store, taskId))
1118
+ return { ok: false, reason: "policy", message: "A task from outside takes the ordinary approval." };
1109
1119
  // An unresolved scope cannot say exactly what would run, so nobody can
1110
1120
  // agree to it (foundations finding 16) — restatement is the road.
1111
1121
  if (scope.profileState === "unresolved") {
@@ -1144,7 +1154,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
1144
1154
  // How the person was proved, beside every yes they give (a vote kept for protected work, or the seal): a password
1145
1155
  // typed now, a re-check now, or a remembered one — with when, the credential generation and the exact digest.
1146
1156
  const proved = () => store.recordAction({ at: now.toISOString(), actor: by, repo: store.lookupRef(taskId)?.repo ?? null, taskId, runId: null, action: APPROVAL_PROOF,
1147
- outcome: authenticated.method, source: "work", detail: approvalProofDetail(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
1157
+ outcome: authenticated.method, source: "work", detail: (fastStart === undefined ? approvalProofDetail : fastProofDetail(fastStart))(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
1148
1158
  if (gate.verdict === "vote") {
1149
1159
  // The same person again adds nothing: no new vote, no proof row (and the approval roads refund a re-check).
1150
1160
  if (gate.already)
@@ -1185,6 +1195,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
1185
1195
  export const APPROVAL_PROOF = "approval proved";
1186
1196
  /** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
1187
1197
  export const approvalProofDetail = (at, generation, digest, local) => `${new Date(at).toISOString()} · generation ${generation}${local === undefined ? "" : ` · local ${local.generation} ${local.transport}`} · digest ${digest}`;
1198
+ /** A fast start's proof: the v1 record proofDetailOf reads, with the marker. `local` keeps the legacy words
1199
+ * localSealStandsSql reads, so a local owner's seal stands on the same rule. */
1200
+ const fastProofDetail = (fastStart) => (at, generation, digest, local) => JSON.stringify({ v: 1, at: new Date(at).toISOString(), generation, digest, fastStart, ...(local === undefined ? {} : { local: `proof · local ${local.generation} ${local.transport} · seal` }) });
1188
1201
  /** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
1189
1202
  export function approvalProofOf(store, taskId, by, digest) {
1190
1203
  const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
@@ -1321,8 +1334,8 @@ export function acceptanceWords(criteria) {
1321
1334
  if (criteria.length === 0)
1322
1335
  return [];
1323
1336
  return [
1324
- ` acceptance ${criteria[0].id}: ${criteria[0].statement} [requires: ${criteria[0].evidence.join(", ")}]`,
1325
- ...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [requires: ${c.evidence.join(", ")}]`),
1337
+ ` done when ${criteria[0].id}: ${criteria[0].statement} [shown by: ${criteria[0].evidence.join(", ")}]`,
1338
+ ...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [shown by: ${c.evidence.join(", ")}]`),
1326
1339
  ];
1327
1340
  }
1328
1341
  /** The route's approval-card lines (v47): the WORKING route the digest
@@ -1340,8 +1353,8 @@ evidence = null) {
1340
1353
  return [
1341
1354
  ` goal ${scope.goal}`,
1342
1355
  ...(scope.outOfScope === null ? [] : [` not this ${scope.outOfScope}`]),
1343
- ...(scope.candidate ? [` candidate ${scope.candidate} — checked out by the machine, no agent runs`] : []),
1344
- ...(scope.touches.length === 0 ? [] : [` touches ${scope.touches.join(", ")}`]),
1356
+ ...(scope.candidate ? [` commit ${scope.candidate.slice(0, 7)} — checked out by Toolroll, no agent runs`] : []),
1357
+ ...(scope.touches.length === 0 ? [] : [` files ${scope.touches.join(", ")}`]),
1345
1358
  ...acceptanceWords(scope.acceptance),
1346
1359
  ...(scope.budgetMicrousd === null
1347
1360
  ? []
@@ -1358,7 +1371,7 @@ evidence = null) {
1358
1371
  ...(scope.profile === null || scope.profile === undefined ? [] : ((terms) => [` reads ${terms.reads}`, ...(terms.mach === null ? [] : [` services ${terms.mach}`])])(sandboxTermsWords(scope.profile))),
1359
1372
  ...releaseTermsWords(scope.releaseTerms ?? null),
1360
1373
  ...(scope.releaseCheck == null ? [] : [` release ${releaseCheckSentence(scope.releaseCheck)}`]),
1361
- ` reference ${scope.digest}`,
1374
+ ` version ${scope.digest.slice(0, 8)} (the full plan version is in --json)`,
1362
1375
  ` approved ${describeApproval(approval)}`,
1363
1376
  ];
1364
1377
  }
@@ -1394,7 +1407,7 @@ function describeApproval(approval) {
1394
1407
  if (approval.approved)
1395
1408
  return `yes, by ${approval.by} at ${approval.at}`;
1396
1409
  if (approval.reason === "changed") {
1397
- return "no — it was approved, then the scope was rewritten; approve it again";
1410
+ return "no — edited after approval; approve it again";
1398
1411
  }
1399
1412
  return "no — nothing will build this until somebody agrees to it";
1400
1413
  }
package/dist/serve.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare function createDecisionServer(options: ServeOptions): DecisionSer
4
4
  export { PAGE_CSS, pinnedTheme, SENSITIVE_INPUT } from "./server/chrome.js";
5
5
  export { redactedPath, SHUTDOWN_WAIT_MS, type ServeOptions } from "./server/http.js";
6
6
  export { TASK_COMPOSER_MODES, type TaskComposerMode } from "./server/render-chat.js";
7
- export { inboxFingerprints, parseInboxTab, queueScript, type InboxTab } from "./server/render-pages.js";
7
+ export { queueScript } from "./server/render-pages.js";
8
8
  export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
9
9
  export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts } from "./server/render-results.js";
10
10
  export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords, type RouteView } from "./server/render-tasks.js";