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
@@ -13,6 +13,7 @@
13
13
  * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
14
14
  * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
15
15
  * console uses. */
16
+ import { routeChangeOf } from "./route-change.js";
16
17
  import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, releaseTaskRef } from "./store.js";
17
18
  import { isAcpProfile } from "./acp-agents.js";
18
19
  import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
@@ -41,13 +42,18 @@ import { PLATFORM_LIMITS } from "./text-limits.js";
41
42
  export { permissionPlainWords };
42
43
  /** The words, said once. */
43
44
  export const DECIDE_LABELS = {
44
- accept: "Accept and finish",
45
- changes: "Request changes",
45
+ accept: "Accept",
46
+ changes: "Send back",
46
47
  retry: "Retry",
47
48
  approve: "Approve & start",
48
49
  "not-now": "Not now",
49
50
  merge: "Merge",
50
51
  look: "Look first",
52
+ /** A result's one link (docs/plans/plain-toolroll.md P1.5). */
53
+ open: "Open result",
54
+ /** A plan card's one button: it shows the full terms, and the Approve sits under them. Never an approval itself. */
55
+ review: "Review plan",
56
+ details: "Details",
51
57
  edit: "Edit",
52
58
  yes: "Yes",
53
59
  cancel: "Cancel",
@@ -390,9 +396,11 @@ export function planCardText(store, taskId, scope, root, channel) {
390
396
  const goal = scope.goal.split("\n").filter(one => one.trim() !== "").slice(0, 2).map(one => phoneText(one, 160));
391
397
  const allowing = [permissionPlainWords(scope.profile), scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`]
392
398
  .filter((one) => one !== null);
399
+ const changed = routeChangeOf(store, taskId);
393
400
  if (channel === "telegram") {
394
401
  return [
395
402
  `Plan ready: ${title(store, taskId)}`,
403
+ ...(changed === null ? [] : [changed]),
396
404
  `Starting allows: ${allowing.join(" · ")}`,
397
405
  "",
398
406
  ...goal,
@@ -405,6 +413,7 @@ export function planCardText(store, taskId, scope, root, channel) {
405
413
  }
406
414
  return [
407
415
  `Plan ready: ${title(store, taskId)}`,
416
+ ...(changed === null ? [] : [changed]),
408
417
  "",
409
418
  ...goal,
410
419
  "",
@@ -433,7 +442,7 @@ export function decideOffer(store, target, who, now, channel, root) {
433
442
  const fresh = resultFresh(store, target.taskId, target.run, who, now, root);
434
443
  if (!fresh.ok)
435
444
  return null;
436
- const look = { label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") };
445
+ const look = { label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") };
437
446
  // A result accepted only with a reason ("Accept anyway") keeps its link: the reason is typed on the result.
438
447
  if (fresh.exception)
439
448
  return null;
@@ -473,15 +482,17 @@ export function decideOffer(store, target, who, now, channel, root) {
473
482
  rows: [[{ act: "approve", run: null, digest: releaseDigest(card), label: "Continue" }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: releaseDigest(card) }]],
474
483
  ...(channel === "telegram" ? { file: { name: card.fileName, text: card.full } } : {}) };
475
484
  }
476
- return { text: planCardText(store, target.taskId, plan.scope, root, channel),
477
- rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
485
+ // One line, one button and Details (docs/plans/plain-toolroll.md P1.5): Review plan shows the full terms in the
486
+ // card, and only there does Approve appear, bound to the same digest.
487
+ return { text: planLineOf(store, target.taskId, ref.id),
488
+ rows: [[{ act: "approve", run: null, digest: plan.digest, label: DECIDE_LABELS.review }], [{ label: DECIDE_LABELS.details, path: chatControlHref("approval", target.taskId) }]] };
478
489
  }
479
490
  case "requests": return requestsOffer(store, target.taskId, who, now, channel, root);
480
491
  case "merge": {
481
492
  const merge = mergeInChat(store, target.taskId, target.run, who.name, now, channel);
482
493
  if (!merge.ok)
483
494
  return null;
484
- return { text: null, rows: [[{ act: "merge", run: target.run, digest: merge.head }], [{ label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") }]] };
495
+ return { text: null, rows: [[{ act: "merge", run: target.run, digest: merge.head }], [{ label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") }]] };
485
496
  }
486
497
  }
487
498
  }
@@ -623,7 +634,7 @@ seat, root) {
623
634
  return { ok: true, card, modeDigest: plan.modeDigest, source: plan.source, scope: plan.scope };
624
635
  }
625
636
  const QUESTIONS = {
626
- accept: name => `Accept and finish "${name}"?`,
637
+ accept: name => `Accept "${name}"?`,
627
638
  retry: name => `Retry "${name}"? It queues another attempt.`,
628
639
  approve: name => `Approve and start "${name}"?`,
629
640
  merge: name => `Merge "${name}"?`,
@@ -712,7 +723,7 @@ function staleWhy(store, row, who, now, channel, root) {
712
723
  /** The links a card keeps once its acts are spent or stale: where to look, never a token. */
713
724
  export function linksFor(target) {
714
725
  switch (target.kind) {
715
- case "result": return [[{ label: DECIDE_LABELS.look, link: { label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") } }]];
726
+ case "result": return [[{ label: DECIDE_LABELS.open, link: { label: DECIDE_LABELS.open, path: chatResultHref(target.taskId, target.run, "changes") } }]];
716
727
  case "merge": return [[{ label: "Open task", link: { label: "Open task", path: chatControlHref("task", target.taskId) } }]];
717
728
  case "plan": return [[{ label: "Review & start", link: { label: "Review & start", path: chatControlHref("approval", target.taskId) } }]];
718
729
  case "failed": return [[{ label: "Open task", link: { label: "Open task", path: chatControlHref("task", target.taskId) } }]];
@@ -740,7 +751,7 @@ export function planWhereNow(store, taskId, who, now, root) {
740
751
  }
741
752
  return { said: `"${name}" is under way.`, rows: [] };
742
753
  }
743
- const QUESTION_STARTS = ["Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \"", "Approve release "];
754
+ const QUESTION_STARTS = ["Accept \"", "Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \"", "Approve release "];
744
755
  /** The card's words without a question this module appended. */
745
756
  function cardBody(shown) {
746
757
  const at = shown.lastIndexOf("\n\n");
@@ -847,6 +858,19 @@ export function applyDecideTap(store, seat, input) {
847
858
  return { ack: "Confirm below.", edit: { text: `${body}\n\n${asked}`,
848
859
  rows: [[{ label: DECIDE_LABELS.yes, token: yes }, { label: DECIDE_LABELS.cancel, token: cancel }]], tokens: [yes, cancel] } };
849
860
  }
861
+ // A plan card's Review plan: the full terms in the card, and under them the Approve (the yes, bound to the same
862
+ // digest the card was minted with, checked again at the tap). Approve is reachable only from here.
863
+ if (row.act === "approve") {
864
+ const plan = planInChat(store, row.taskId, who.name, now, seat.channel, input.root);
865
+ if (!plan.ok)
866
+ return stale(plan.why);
867
+ spend();
868
+ const spec = { act: "approve", run: null, digest: row.digest };
869
+ const yes = mint(store, seat, input.message, spec, row.taskId, "yes", now);
870
+ const later = mint(store, seat, input.message, { act: "not-now", run: null, digest: row.digest }, row.taskId, "offer", now);
871
+ return { ack: "Read the plan, then approve.", edit: { text: `${planCardText(store, row.taskId, plan.scope, input.root, seat.channel)}\n\n${QUESTIONS.approve(name)} This approves exactly this plan.`,
872
+ rows: [[{ label: DECIDE_LABELS.approve, token: yes }], [{ label: DECIDE_LABELS.edit, link: { label: DECIDE_LABELS.edit, path: chatControlHref("approval", row.taskId) } }, { label: DECIDE_LABELS["not-now"], token: later }]], tokens: [yes, later] } };
873
+ }
850
874
  // The first tap asks; only the yes acts, bound to exactly what this button was.
851
875
  spend();
852
876
  const spec = { act: row.act, run: row.run, digest: row.digest };
@@ -1007,3 +1031,7 @@ export function decideTargetOf(row) {
1007
1031
  return { kind: "failed", taskId: row.taskId };
1008
1032
  return null;
1009
1033
  }
1034
+ /** A plan card's one line: the task's number, "plan ready", and its title (docs/plans/plain-toolroll.md P1.5). */
1035
+ function planLineOf(store, taskId, number) {
1036
+ return `#${number}: plan ready · ${title(store, taskId)}`;
1037
+ }
@@ -102,7 +102,7 @@ export function quietCardView(store, taskRefs, now, root, viewer) {
102
102
  const heading = `${lines.length} tasks`;
103
103
  const projects = [...new Set(lines.map(one => projectLabel(one.project)))];
104
104
  const text = [heading, ...lines.map(one => `${one.icon} ${phoneText(one.title, 60)} · ${one.status}`), "", projects.join(", ")].join("\n");
105
- return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/work" } };
105
+ return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/tasks" } };
106
106
  }
107
107
  /** Facts that carry their own controls or must stand alone (a security alert, a decision, a flow card, a
108
108
  * screenshot, an acceptance packet, a youiyoux pass's card) are never folded into a batch. */
@@ -177,7 +177,7 @@ export function finishedView(store, batch, now, root, viewer) {
177
177
  return null;
178
178
  const text = batchLine(lines.map(one => one.fact));
179
179
  if (lines.length > 1)
180
- return { text, entities: [], link: { label: "Open", path: "/work" } };
180
+ return { text, entities: [], link: { label: "Open", path: "/tasks" } };
181
181
  const target = lines[0].target ?? null;
182
182
  return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
183
183
  }
@@ -244,7 +244,7 @@ export function enqueueEveningDigests(store, now, root) {
244
244
  if (body === null)
245
245
  return;
246
246
  if (store.enqueueNotification({ dedupeKey: `digest:evening:${person.account}:${day}`, kind: "evening-digest", subject: "Evening digest", body,
247
- link: "/work", source: { installation: true }, recipient: person.account }, now))
247
+ link: "/tasks", source: { installation: true }, recipient: person.account }, now))
248
248
  queued++;
249
249
  });
250
250
  }
@@ -39,7 +39,7 @@ export type FinishedFact = {
39
39
  /** The two buttons under a result ready for a person: the real next step, then a look first. */
40
40
  export declare const READY_ACTIONS: {
41
41
  readonly merge: "Merge";
42
- readonly complete: "Accept and finish";
42
+ readonly complete: "Accept";
43
43
  readonly look: "Look first";
44
44
  };
45
45
  /** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */
@@ -48,7 +48,7 @@ export function inSentence(summary) {
48
48
  return /^\p{Lu}\p{Ll}/u.test(summary) ? summary[0].toLowerCase() + summary.slice(1) : summary;
49
49
  }
50
50
  /** The two buttons under a result ready for a person: the real next step, then a look first. */
51
- export const READY_ACTIONS = { merge: "Merge", complete: "Accept and finish", look: "Look first" };
51
+ export const READY_ACTIONS = { merge: "Merge", complete: "Accept", look: "Look first" };
52
52
  /** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */
53
53
  export function asksToFinish(fact) {
54
54
  return fact.update == null && fact.headline === "Ready for review" && !fact.report;
@@ -67,7 +67,7 @@ export function finishedLine(fact) {
67
67
  if (fact.report)
68
68
  return fact.lead === true ? `I wrote up ${lower}. The report is ready to read.` : `${name}: the report is ready to read.`;
69
69
  if (fact.checks === "passed")
70
- return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept and finish it?"}`;
70
+ return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept it?"}`;
71
71
  if (fact.checks === "off")
72
72
  return `${ready}. Its checks run at release, so look it over first.`;
73
73
  return `${ready}, but no tests ran. Look it over first.`;
@@ -87,6 +87,8 @@ export type CheckFactsInput = {
87
87
  * nothing reads these for a verdict, a rerun or an approval.
88
88
  */
89
89
  export declare function recordCheckFacts(store: Store, root: string, input: CheckFactsInput): void;
90
+ /** The storage sweep's own step: results older than the retention window go; their fact rows stay. */
91
+ export declare function pruneCheckResults(store: Store, now: Date): number;
90
92
  /** A check's recorded facts, or null. */
91
93
  export type CheckFact = {
92
94
  run: number;
@@ -117,7 +119,7 @@ export declare function checkResultsOf(store: Store, runId: number): {
117
119
  * accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
118
120
  * most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
119
121
  */
120
- export declare function learnRequestFor(store: Store, repo: string, now: Date): {
122
+ export declare function learnRequestFor(store: Store, repo: string, now: Date, runId?: number): {
121
123
  learn: string[];
122
124
  leftOut: number;
123
125
  };
@@ -25,6 +25,8 @@ import { createHash } from "node:crypto";
25
25
  import { failingTestsOf } from "./gate-failure.js";
26
26
  import { checkContextOf, projectGitDir, treeFactsOf } from "./check-report.js";
27
27
  import { readVerifiedArtifact } from "./evidence.js";
28
+ import { recordObservations } from "./flaky.js";
29
+ import { considerLessonNote, markTalliesPartial, revisedFailures, tallyCheck } from "./lesson-notes.js";
28
30
  import { CHECK_REPORT_PREFIX, isExactCase, learnLineSchema, learnRequest, LEARN_MAX_BYTES, LEARN_PREFIX } from "./contracts/release-check.js";
29
31
  import { nightlyTaskRef, releaseTaskRef } from "./store.js";
30
32
  /** The sources a result can come from, and the states it can have. */
@@ -36,6 +38,8 @@ export const UNEXPLAINED = "unexplained";
36
38
  export const MAX_RESULTS_PER_CHECK = 20_000;
37
39
  /** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
38
40
  export const OBSERVATION_WATCH_MS = 90 * 24 * 60 * 60 * 1000;
41
+ /** How long per-identity results are kept (docs/plans/learning-loops.md, "Per table"). */
42
+ const RESULT_RETAIN_MS = 180 * 24 * 60 * 60 * 1000;
39
43
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
40
44
  /**
41
45
  * The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
@@ -225,16 +229,47 @@ export function recordCheckFacts(store, root, input) {
225
229
  kept.set(key, row);
226
230
  }
227
231
  const at = input.now.toISOString();
228
- store.transact(() => {
229
- const inserted = store.handle.prepare(`INSERT INTO check_fact (run, repo, task_ref, head, tree, command_digest, access, harness_digest, lockfile, conditions_digest,
232
+ let recorded = false;
233
+ try {
234
+ store.transact(() => {
235
+ const inserted = store.handle.prepare(`INSERT INTO check_fact (run, repo, task_ref, head, tree, command_digest, access, harness_digest, lockfile, conditions_digest,
230
236
  conditions_class, purpose, learn_state, learn_why, recorded_at) SELECT id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM run WHERE id = ?
231
237
  ON CONFLICT (run) DO NOTHING`).run(input.repo, input.taskRef, conditions?.head ?? null, comparable ? conditions.tree : null, conditions?.commandDigest ?? null, conditions?.access ?? null, comparable ? conditions.harnessDigest : null, comparable ? conditions.lockfile : null, comparable ? conditions.conditionsDigest : null, comparable ? conditions.conditionsClass : null, checkPurposeOf(store, input.taskRef), learnState, learnState === "not-recorded" ? learnWhy ?? "not recorded" : null, at, input.runId);
232
- if (Number(inserted.changes) !== 1)
233
- return;
234
- const insert = store.handle.prepare("INSERT INTO check_result (run, identity, state, source, recorded_at) VALUES (?, ?, ?, ?, ?)");
235
- for (const row of [...kept.values()].slice(0, MAX_RESULTS_PER_CHECK))
236
- insert.run(input.runId, row.identity, row.state, row.source, at);
237
- });
238
+ if (Number(inserted.changes) !== 1)
239
+ return;
240
+ const insert = store.handle.prepare("INSERT INTO check_result (run, identity, state, source, recorded_at) VALUES (?, ?, ?, ?, ?)");
241
+ for (const row of [...kept.values()].slice(0, MAX_RESULTS_PER_CHECK))
242
+ insert.run(input.runId, row.identity, row.state, row.source, at);
243
+ // In the same transaction: what this check adds to the flaky-test list and to accepted notes' tallies, advisory like
244
+ // everything here.
245
+ const observations = recordObservations(store, input.runId, input.now);
246
+ if (input.repo !== null)
247
+ tallyCheck(store, input.runId, input.repo, observations, input.now);
248
+ recorded = true;
249
+ });
250
+ }
251
+ catch (error) {
252
+ // Not every check was counted this month: its tallies say so.
253
+ if (input.repo !== null) {
254
+ try {
255
+ markTalliesPartial(store, input.repo, input.now);
256
+ }
257
+ catch { /* the note reads as it was */ }
258
+ }
259
+ throw error;
260
+ }
261
+ // A person's lesson note, when this check is what passed a revision. Advisory: nothing here reaches the check.
262
+ if (recorded) {
263
+ try {
264
+ considerLessonNote(store, root, input.runId, input.now);
265
+ }
266
+ catch { /* no note */ }
267
+ }
268
+ }
269
+ /** The storage sweep's own step: results older than the retention window go; their fact rows stay. */
270
+ export function pruneCheckResults(store, now) {
271
+ const cut = new Date(now.getTime() - RESULT_RETAIN_MS).toISOString();
272
+ return Number(store.handle.prepare("DELETE FROM check_result WHERE recorded_at < ?").run(cut).changes);
238
273
  }
239
274
  export function checkFactOf(store, runId) {
240
275
  const row = store.handle.prepare("SELECT * FROM check_fact WHERE run = ?").get(runId);
@@ -255,7 +290,7 @@ export function checkResultsOf(store, runId) {
255
290
  * accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
256
291
  * most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
257
292
  */
258
- export function learnRequestFor(store, repo, now) {
293
+ export function learnRequestFor(store, repo, now, runId) {
259
294
  const since = new Date(now.getTime() - OBSERVATION_WATCH_MS).toISOString();
260
295
  let tracked;
261
296
  // Advisory: a read that fails asks about nothing, and the check runs as it would have.
@@ -267,6 +302,17 @@ export function learnRequestFor(store, repo, now) {
267
302
  catch {
268
303
  return { learn: [], leftOut: 0 };
269
304
  }
270
- const learn = learnRequest(tracked);
271
- return { learn, leftOut: tracked.length - learn.length };
305
+ // First, the failed cases of the check this run revises (lesson-notes.ts rule 4).
306
+ let revised = [];
307
+ if (runId !== undefined) {
308
+ try {
309
+ revised = revisedFailures(store, runId);
310
+ }
311
+ catch {
312
+ revised = [];
313
+ }
314
+ }
315
+ const all = [...new Set([...revised, ...tracked])];
316
+ const learn = learnRequest(all);
317
+ return { learn, leftOut: all.length - learn.length };
272
318
  }
@@ -1,4 +1,5 @@
1
1
  import { type CheckReport } from "./contracts/release-check.js";
2
+ import { type CheckLoadRecord } from "./check-load.js";
2
3
  import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT, type FailureClassName } from "./release.js";
3
4
  import type { Store } from "./store.js";
4
5
  export type FailureClass = FailureClassName;
@@ -20,12 +21,16 @@ export type CheckFacts = {
20
21
  failure?: string;
21
22
  };
22
23
  coverage?: string | undefined;
24
+ /** The approved limit of the command the receipt names (receipt.command.timeoutMs): a load record must be for it. */
25
+ grantMs?: number | undefined;
23
26
  };
24
27
  /**
25
- * One failed check, classified. `facts` come from the plane's sealed receipt; `report` (the check's own report, sealed
26
- * when valid) only ever picks among the classes that block. Null when the check passed.
28
+ * One failed check, classified. `facts` come from the plane's sealed receipt and `load` from the plane's own sealed load
29
+ * record (check-load.ts); `report` (the check's own report, sealed when valid) only ever picks among the classes that
30
+ * block — a timeout is excused only when the report names nothing else that failed, and only by the plane's own facts:
31
+ * its load readings cut the budget, and the check ran into its limit by the plane's own clock. Null when it passed.
27
32
  */
28
- export declare function classifyCheckFailure(facts: CheckFacts, report: CheckReport | null): ClassifiedFailure | null;
33
+ export declare function classifyCheckFailure(facts: CheckFacts, report: CheckReport | null, load?: CheckLoadRecord | null): ClassifiedFailure | null;
29
34
  /** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
30
35
  * never silently part of a pass. Null when there is no valid report to read it from: unknown, never "no retries". */
31
36
  export declare function retriedInside(report: CheckReport | null): string[] | null;
@@ -45,6 +50,20 @@ export declare const SETUP_REPLAY_RETRY = "the whole check, rerun after the appr
45
50
  * missing program; a receipt that doesn't classify it (an older one) says so, never that it was a launch failure.
46
51
  */
47
52
  export declare function setupReplayWords(first: unknown): string;
53
+ /**
54
+ * An attempt interrupted before its candidate was even loaded (run 3326: the worker stopped under it — "The attempt was
55
+ * stopped before the prepared candidate was loaded."): it ran no check, so it is no check failure of the commit. Only
56
+ * the plane's own records say so, all of them at once: the run ended failed/interrupted, recorded no commit, and has no
57
+ * check attempt (sealed before any check command runs), no recorded check and no check log or receipt. Any attempt that
58
+ * got as far as its check keeps every rule below.
59
+ */
60
+ export declare function ranNoCheck(store: Store, row: {
61
+ id: number;
62
+ outcome: unknown;
63
+ reason: unknown;
64
+ head: unknown;
65
+ checkRow: unknown;
66
+ }): boolean;
48
67
  /**
49
68
  * Whether a passing check of a release task stands, given every earlier attempt on the same candidate and signed scope:
50
69
  * each earlier failure must be confirmed infrastructure the release itself reran (at most two), and nothing in the
@@ -5,7 +5,8 @@
5
5
  * deterministic typecheck, build, a failing test, a scripted journey, an upgrade, a required test gone:
6
6
  * nothing tolerated, never rerun to pass; the branch is fixed and the new commit released.
7
7
  * infrastructure independently confirmed by the plane itself: the check never started, this worker lost the
8
- * build, Toolroll's own shutdown stopped it — or, on GitHub, a job that failed before any of the
8
+ * build, Toolroll's own shutdown stopped it, it ran out of time on a machine the plane measured
9
+ * busy enough to cut its budget to half or less (check-load.ts) — or, on GitHub, a job that failed before any of the
9
10
  * workflow's own steps ran. At most one rerun per incident and two per candidate, under the same
10
11
  * approval; a pass after them counts.
11
12
  * unknown exit 1 with no failing test named, an unexplained timeout or signal, no result at all: an error
@@ -20,10 +21,12 @@
20
21
  */
21
22
  import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
22
23
  import { readCheckReport } from "./contracts/release-check.js";
24
+ import { busyMachineTimeout, CHECK_LOAD_CAPTURE, readCheckLoad } from "./check-load.js";
23
25
  import { regateTask } from "./dispose.js";
24
26
  import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT } from "./release.js";
25
27
  import { releaseCheckScope } from "./scope.js";
26
- import { verificationEvidence } from "./verification-evidence.js";
28
+ import { isVerificationReceipt, verificationEvidence } from "./verification-evidence.js";
29
+ import { checkAttemptOf } from "./store.js";
27
30
  /** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
28
31
  export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
29
32
  /** The ledger action that records each rerun the release asked for, and why. */
@@ -35,11 +38,15 @@ const PLANE_INFRA = {
35
38
  "custody-lost": "this worker lost the build before the check could finish",
36
39
  "stopped-by-toolroll": "Toolroll's own shutdown stopped the check before it finished",
37
40
  };
41
+ /** How a timeout on a busy machine reads (its rerun is the release terms' "Toolroll's own machine fails"). */
42
+ const busyWords = (why) => `${why}: Toolroll's own machine, so this commit's check runs again`;
38
43
  /**
39
- * One failed check, classified. `facts` come from the plane's sealed receipt; `report` (the check's own report, sealed
40
- * when valid) only ever picks among the classes that block. Null when the check passed.
44
+ * One failed check, classified. `facts` come from the plane's sealed receipt and `load` from the plane's own sealed load
45
+ * record (check-load.ts); `report` (the check's own report, sealed when valid) only ever picks among the classes that
46
+ * block — a timeout is excused only when the report names nothing else that failed, and only by the plane's own facts:
47
+ * its load readings cut the budget, and the check ran into its limit by the plane's own clock. Null when it passed.
41
48
  */
42
- export function classifyCheckFailure(facts, report) {
49
+ export function classifyCheckFailure(facts, report, load = null) {
43
50
  const result = facts.result;
44
51
  const passedCoverage = facts.coverage === undefined || facts.coverage === "full" || facts.coverage === "composed";
45
52
  if (result.configured && result.ran === true && result.exitCode === 0 && passedCoverage)
@@ -51,8 +58,12 @@ export function classifyCheckFailure(facts, report) {
51
58
  const infra = PLANE_INFRA[failure];
52
59
  if (infra !== undefined)
53
60
  return { class: "infrastructure", why: infra };
54
- if (failure === "timed-out" || failure === "retry-timed-out")
55
- return { class: "unknown", why: "the check timed out, and nothing says why" };
61
+ if (failure === "timed-out" || failure === "retry-timed-out") {
62
+ // The plane stopped it at its limit. With the plane's own record of a cut budget, the machine's fault; otherwise
63
+ // (a normal budget, or no record) a timeout nothing explains, as ever.
64
+ const busy = facts.grantMs === undefined ? null : busyMachineTimeout(load, facts.grantMs, true);
65
+ return busy !== null ? { class: "infrastructure", why: busyWords(busy) } : { class: "unknown", why: "the check timed out, and nothing says why" };
66
+ }
56
67
  if (failure === "baseline-unverifiable")
57
68
  return { class: "unknown", why: "the last release Toolroll recorded couldn't be verified, so what the check owed is unknown and it wasn't run" };
58
69
  if (failure === "no-result")
@@ -65,10 +76,14 @@ export function classifyCheckFailure(facts, report) {
65
76
  if (report === null)
66
77
  return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} and gave no report naming what failed` };
67
78
  const deterministic = [], agent = [], unexplained = [];
79
+ // Whether everything unexplained is a part that ran out of time (never a signal, never an error no test owned).
80
+ let onlyTimeouts = true;
68
81
  for (const part of report.parts) {
69
82
  if (!part.selected || !part.ran || part.code === null)
70
83
  continue;
71
84
  if (part.timedOut || part.signal != null) {
85
+ if (!part.timedOut || part.signal != null)
86
+ onlyTimeouts = false;
72
87
  unexplained.push(`${part.part} ${part.timedOut ? "timed out" : `was ended by ${part.signal}`}`);
73
88
  continue;
74
89
  }
@@ -78,8 +93,10 @@ export function classifyCheckFailure(facts, report) {
78
93
  for (const file of report.units?.files ?? [])
79
94
  if (file.state === "failed")
80
95
  deterministic.push(`${file.file} failed`);
81
- if ((report.units?.runErrors ?? 0) > 0 && !(report.units?.files ?? []).some(file => file.state === "failed"))
96
+ if ((report.units?.runErrors ?? 0) > 0 && !(report.units?.files ?? []).some(file => file.state === "failed")) {
97
+ onlyTimeouts = false;
82
98
  unexplained.push(`${report.units.runErrors} unit errors no test owned`);
99
+ }
83
100
  for (const run of report.journeys)
84
101
  for (const group of run.groups) {
85
102
  if (group.state !== "failed")
@@ -94,8 +111,14 @@ export function classifyCheckFailure(facts, report) {
94
111
  return { class: "deterministic", why: list(deterministic) };
95
112
  if (agent.length > 0)
96
113
  return { class: "agent-behaviour", why: list(agent) };
97
- if (unexplained.length > 0)
114
+ if (unexplained.length > 0) {
115
+ // Only parts that ran out of time, and the plane's own record says the machine cut the budget and the check ran
116
+ // into its limit: the machine's fault. The report can only withhold this (by naming anything else), never grant it.
117
+ const busy = onlyTimeouts && facts.grantMs !== undefined ? busyMachineTimeout(load, facts.grantMs, false) : null;
118
+ if (busy !== null)
119
+ return { class: "infrastructure", why: busyWords(`${list(unexplained)}; ${busy}`) };
98
120
  return { class: "unknown", why: list(unexplained) };
121
+ }
99
122
  return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} with no failing test identified` };
100
123
  }
101
124
  /** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
@@ -127,7 +150,10 @@ export function classifyRunCheck(store, root, runId) {
127
150
  return { class: "unknown", why: "the check's receipt names no result" };
128
151
  const sealed = readSealedJson(store, root, runId, CHECK_REPORT_CAPTURE);
129
152
  const parsed = sealed === null ? null : readCheckReport(sealed.value);
130
- return classifyCheckFailure({ result: receipt.result, coverage: receipt.coverage }, parsed?.ok === true ? parsed.value : null);
153
+ // The plane's own load record (check-load.ts): absent on older runs, and then nothing is excused.
154
+ const loadSealed = readSealedJson(store, root, runId, CHECK_LOAD_CAPTURE);
155
+ const grantMs = typeof receipt.command?.timeoutMs === "number" ? receipt.command.timeoutMs : undefined;
156
+ return classifyCheckFailure({ result: receipt.result, coverage: receipt.coverage, grantMs }, parsed?.ok === true ? parsed.value : null, loadSealed === null ? null : readCheckLoad(loadSealed.value));
131
157
  }
132
158
  /**
133
159
  * What passed only on a retry inside a run's check (eval-harness review 1, finding 3): the summary sealed into its
@@ -174,12 +200,27 @@ export function setupReplayWords(first) {
174
200
  }
175
201
  return `${SETUP_REPLAY_RETRY} (its first attempt's failure isn't classified)`;
176
202
  }
177
- /** The task's finished builder runs under `digest`, oldest first, each with whether its check passed. */
203
+ /**
204
+ * An attempt interrupted before its candidate was even loaded (run 3326: the worker stopped under it — "The attempt was
205
+ * stopped before the prepared candidate was loaded."): it ran no check, so it is no check failure of the commit. Only
206
+ * the plane's own records say so, all of them at once: the run ended failed/interrupted, recorded no commit, and has no
207
+ * check attempt (sealed before any check command runs), no recorded check and no check log or receipt. Any attempt that
208
+ * got as far as its check keeps every rule below.
209
+ */
210
+ export function ranNoCheck(store, row) {
211
+ if (row.outcome !== "failed" || row.reason !== "interrupted" || row.head !== null || row.checkRow !== null)
212
+ return false;
213
+ if (checkAttemptOf(store, row.id) !== null)
214
+ return false;
215
+ return !store.artifactsFor(row.id).some(artifact => artifact.kind === "check-log" || isVerificationReceipt(artifact));
216
+ }
217
+ /** The task's finished builder runs under `digest`, oldest first, each with whether its check passed or never ran. */
178
218
  function builderRuns(store, taskRef, digest) {
179
- return store.handle.prepare(`SELECT r.id, r.outcome, r.scope_digest, r.finished_at, c.status FROM run r LEFT JOIN run_check c ON c.run = r.id
219
+ return store.handle.prepare(`SELECT r.id, r.outcome, r.reason, r.head_revision, r.scope_digest, r.finished_at, c.status, c.run AS check_row FROM run r LEFT JOIN run_check c ON c.run = r.id
180
220
  WHERE r.task_ref = ? AND r.role = 'builder' AND r.finished_at IS NOT NULL AND r.scope_digest = ? ORDER BY r.id`).all(taskRef, digest)
181
221
  .map(row => ({ id: Number(row["id"]), outcome: row["outcome"] === null ? null : String(row["outcome"]), scopeDigest: String(row["scope_digest"]), finishedAt: String(row["finished_at"]),
182
- passed: (row["outcome"] === "built" || row["outcome"] === "no-change") && row["status"] === "passed" }));
222
+ passed: (row["outcome"] === "built" || row["outcome"] === "no-change") && row["status"] === "passed",
223
+ noCheck: ranNoCheck(store, { id: Number(row["id"]), outcome: row["outcome"], reason: row["reason"], head: row["head_revision"] ?? null, checkRow: row["check_row"] ?? null }) }));
183
224
  }
184
225
  /** The reruns the release asked for on this task (each source run once): run id → its class. */
185
226
  function reruns(store, taskId) {
@@ -208,6 +249,9 @@ export function releaseCheckStanding(store, root, input) {
208
249
  for (const one of builderRuns(store, ref.id, digest)) {
209
250
  if (one.id >= input.run || one.passed)
210
251
  continue;
252
+ // Interrupted before its candidate was loaded: no check ran, so there is no check failure to stand against a pass.
253
+ if (one.noCheck)
254
+ continue;
211
255
  const classified = one.outcome === "built" || one.outcome === "no-change" ? classifyRunCheck(store, root, one.id) : { class: "unknown", why: `its build ended ${one.outcome ?? "without an outcome"}` };
212
256
  if (classified === null)
213
257
  continue;
@@ -24,6 +24,10 @@ export type CheckSettingsView = {
24
24
  batch?: {
25
25
  on: boolean;
26
26
  };
27
+ /** Small tasks start right away (fast-start.ts). */
28
+ fast?: {
29
+ on: boolean;
30
+ };
27
31
  said: string | null;
28
32
  problem: string | null;
29
33
  };
@@ -48,7 +48,16 @@ export function checkSettingsHtml(view) {
48
48
  body: view.full === null
49
49
  ? joinHtml([note("Approve one from the command line:"), codeLine(`toolroll verify set --repo … --command "…"`)])
50
50
  : command(view.full) });
51
- return joinHtml([head, level, batchHtml(view), reviewHtml(view), quick, full]);
51
+ return joinHtml([head, level, fastHtml(view), batchHtml(view), reviewHtml(view), quick, full]);
52
+ /** Small tasks: one state, one sentence, and its one change behind a disclosure (the password). */
53
+ function fastHtml(view) {
54
+ if (view.fast === undefined)
55
+ return "";
56
+ const act = view.fast.on ? "Turn off" : "Turn on";
57
+ return settingsSection({ id: "small-tasks", title: "Small tasks start right away", state: html `<span data-small-tasks="${view.fast.on ? "on" : "off"}"> · ${view.fast.on ? "On" : "Off"}</span>`,
58
+ lead: "New task shows the exact terms and Build approves them, with no plan step. It takes a person, and only small, ordinary changes.",
59
+ body: view.canChange ? disclose(act, form("small-tasks-form", "small-tasks", html ``, { label: act, variant: "outline" }, { on: view.fast.on ? "0" : "1" })) : "" });
60
+ }
52
61
  /** Batch checks: one state and one sentence; turning it on restates exactly what happens before the password. */
53
62
  function batchHtml(view) {
54
63
  const batch = view.batch;
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ export declare const CHECK_LOAD_CAPTURE = "release check load v1";
3
+ /** One reading of the machine: its cores and one-minute load (NaN where it isn't measured: Windows reports 0). */
4
+ export type MachineSample = {
5
+ cores: number;
6
+ load: number;
7
+ };
8
+ export declare const machineSample: (platform?: NodeJS.Platform, read?: () => number[], cores?: () => number) => MachineSample;
9
+ /** The check's budget on a quiet machine: three quarters of the cores, at least 2 (release-check.mjs checkBudget). */
10
+ export declare const normalFree: (cores: number) => number;
11
+ /** The check's budget under `load` (release-check.mjs checkBudget's `free`). Unmeasured load budgets from the cores. */
12
+ export declare const freeAt: (cores: number, load: number) => number;
13
+ /** Cut: the budget is at most half its normal size. Unmeasured load is never cut. */
14
+ export declare const budgetCut: (cores: number, load: number) => boolean;
15
+ /** A release check's limit never goes past this, however busy the machine. */
16
+ export declare const CHECK_LIMIT_CEILING_MS: number;
17
+ /** …nor past this many times the approved limit. */
18
+ export declare const CHECK_LIMIT_MAX_FACTOR = 2;
19
+ /**
20
+ * The limit a release check runs under: the approved one, unless the machine's load at its start cuts the check's
21
+ * budget to half or less — then the approved limit times normal/granted, at most twice it and never past the ceiling
22
+ * (an approved limit already at or past the ceiling is never extended). `why` says which, in words.
23
+ */
24
+ export declare function checkLimitFor(grantMs: number, sample: MachineSample): {
25
+ limitMs: number;
26
+ why: string;
27
+ };
28
+ /** When a check ends itself before its limit (release-check.mjs checkDeadline, from its start): src/check-load.test.ts
29
+ * holds the two equal. A check that ran at least this long ran into its own limit. */
30
+ export declare const selfStopAfter: (limitMs: number) => number;
31
+ /** The samples read for the budget the check started with: the check waits up to ten minutes for the load to drop
32
+ * (release-check.mjs), so the least load in its first eleven — before its own load counts — is what it budgeted from at
33
+ * best. The least, so a check that found a quieter moment is never called cut. */
34
+ export declare const CUT_WINDOW_MS: number;
35
+ declare const recordSchema: z.ZodObject<{
36
+ version: z.ZodLiteral<1>;
37
+ cores: z.ZodInt;
38
+ grantMs: z.ZodInt;
39
+ limitMs: z.ZodInt;
40
+ why: z.ZodString;
41
+ startedAt: z.ZodString;
42
+ elapsedMs: z.ZodNullable<z.ZodInt>;
43
+ samples: z.ZodArray<z.ZodObject<{
44
+ atMs: z.ZodInt;
45
+ load: z.ZodNullable<z.ZodNumber>;
46
+ }, z.core.$strict>>;
47
+ }, z.core.$strict>;
48
+ export type CheckLoadRecord = z.infer<typeof recordSchema>;
49
+ /** A sealed record read back, or null when it isn't one (an older run has none: then nothing is excused). */
50
+ export declare function readCheckLoad(value: unknown): CheckLoadRecord | null;
51
+ /**
52
+ * Why a release check that ran out of time did so because the machine was busy — or null. Every fact is the plane's:
53
+ * its load readings in the first eleven minutes (the least of them) cut the check's budget to half or less, and the
54
+ * check's last attempt ran into its own limit (at least `selfStopAfter(limitMs)`), or the plane stopped it at the limit
55
+ * (`stoppedAtLimit`). A check with its normal budget that times out is never excused here: a real hang stays a failure.
56
+ */
57
+ export declare function busyMachineTimeout(record: CheckLoadRecord | null, grantMs: number, stoppedAtLimit: boolean): string | null;
58
+ /**
59
+ * Watches one release check: the limit at its start, the load every 15 s while it runs, and how long its last attempt
60
+ * took. Advisory to the check itself (the limit is a plane value the check is told), and the only source of the record.
61
+ */
62
+ export declare class CheckLoadWatch {
63
+ private readonly grantMs;
64
+ private readonly options;
65
+ private readonly started;
66
+ private readonly samples;
67
+ private readonly timer;
68
+ private readonly cores;
69
+ private attemptStart;
70
+ private elapsed;
71
+ readonly limitMs: number;
72
+ readonly why: string;
73
+ constructor(grantMs: number, options?: {
74
+ sample?: () => MachineSample;
75
+ clock?: () => number;
76
+ everyMs?: number;
77
+ });
78
+ private push;
79
+ /** Around each attempt of the command: the last one's time is the record's. */
80
+ attemptStarted(): void;
81
+ attemptEnded(): void;
82
+ /** Stops the readings and gives the record to seal. */
83
+ finish(): CheckLoadRecord;
84
+ }
85
+ export {};