toolroll 0.9.92 → 0.9.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +4 -0
  5. package/dist/agent-fence.js +4 -1
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
@@ -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.`;
@@ -0,0 +1,126 @@
1
+ import { type CheckReport } from "./contracts/release-check.js";
2
+ import { type Store, type VerifyCommand } from "./store.js";
3
+ /** The sources a result can come from, and the states it can have. */
4
+ export declare const CHECK_RESULT_SOURCES: readonly ["receipt", "report", "log", "learn"];
5
+ export type CheckResultSource = typeof CHECK_RESULT_SOURCES[number];
6
+ export type CheckResultState = "failed" | "passed" | "skipped" | "passed-on-retry";
7
+ /** The receipt's whole-check identity, and the one a failed check with nothing else named records. */
8
+ export declare const WHOLE_CHECK = "check";
9
+ export declare const UNEXPLAINED = "unexplained";
10
+ /** How many result rows one check records at most (a report's own bounds are far above any real check). */
11
+ export declare const MAX_RESULTS_PER_CHECK = 20000;
12
+ /** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
13
+ export declare const OBSERVATION_WATCH_MS: number;
14
+ /** What a check ran under, captured when its command started. `tree` null: its commit couldn't be read from verified
15
+ * objects, and nothing about the check is comparable. */
16
+ export type CheckConditions = {
17
+ head: string;
18
+ tree: string | null;
19
+ commandDigest: string;
20
+ access: string;
21
+ harnessDigest: string | null;
22
+ lockfile: string | null;
23
+ conditionsDigest: string | null;
24
+ conditionsClass: string | null;
25
+ problem: string | null;
26
+ };
27
+ /**
28
+ * The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
29
+ * access, and the values of `envNames` (the variables the check inherits, builder.ts SETUP_ENV_ALLOWLIST) as this
30
+ * process holds them now — what the command is given. Hashed with checkContextOf's fields; the class reads
31
+ * `<platform>-<arch>-node<major>`. Values are hashed, never stored.
32
+ */
33
+ export declare function captureCheckConditions(input: {
34
+ repo: string | null;
35
+ head: string;
36
+ command: VerifyCommand;
37
+ access: string;
38
+ envNames: readonly string[];
39
+ env?: NodeJS.ProcessEnv;
40
+ }): CheckConditions;
41
+ /** Why the task's check ran: a release's or the nightly's purpose, read from the plane's own records. */
42
+ export declare function checkPurposeOf(store: Store, taskRef: number): "release" | "nightly" | "ordinary";
43
+ /** The advisory learning line of a check's output, read strictly: exactly one line carrying this run's nonce, at most
44
+ * 64 KiB, before the report, matching its contract, naming only requested exact cases, each once. Anything else is
45
+ * not recorded, with why; that loses learning evidence only. */
46
+ export declare function learningLinesOf(stdout: string, nonce: string, asked: readonly string[]): {
47
+ ok: true;
48
+ results: {
49
+ identity: string;
50
+ state: "passed" | "failed" | "skipped";
51
+ }[];
52
+ } | {
53
+ ok: false;
54
+ why: string;
55
+ };
56
+ type Row = {
57
+ identity: string;
58
+ state: CheckResultState;
59
+ source: CheckResultSource;
60
+ };
61
+ /** The report's lines as results: every case it names; a file, group, version or part only when it didn't simply pass. */
62
+ export declare function reportResults(report: CheckReport): Row[];
63
+ export type CheckFactsInput = {
64
+ runId: number;
65
+ taskRef: number;
66
+ repo: string | null;
67
+ /** Captured when the command started; null when no command started (nothing comparable was run). */
68
+ conditions: CheckConditions | null;
69
+ /** What the plane recorded as the check's own outcome (run_check), unchanged. */
70
+ receipt: {
71
+ status: "passed" | "failed" | "not-run";
72
+ };
73
+ /** The sealed report, when the check gave a valid one. */
74
+ report: CheckReport | null;
75
+ /** The check's handshake facts, when it had one: its nonce and what it was asked about for learning. */
76
+ attempt: {
77
+ nonce: string;
78
+ learn: readonly string[];
79
+ } | null;
80
+ /** The check's final command output, for the learning line (the same bytes the report was read from). */
81
+ stdout: string | null;
82
+ now: Date;
83
+ };
84
+ /**
85
+ * Record what a settled check ran under and reported, once (first write wins), in one transaction: its fact row and
86
+ * every result row. Called at disposition by the builder right after it records the check's own outcome. Advisory:
87
+ * nothing reads these for a verdict, a rerun or an approval.
88
+ */
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;
92
+ /** A check's recorded facts, or null. */
93
+ export type CheckFact = {
94
+ run: number;
95
+ repo: string | null;
96
+ taskRef: number;
97
+ head: string | null;
98
+ tree: string | null;
99
+ commandDigest: string | null;
100
+ access: string | null;
101
+ harnessDigest: string | null;
102
+ lockfile: string | null;
103
+ conditionsDigest: string | null;
104
+ conditionsClass: string | null;
105
+ purpose: "release" | "nightly" | "ordinary";
106
+ learnState: "recorded" | "not-recorded" | "not-asked";
107
+ learnWhy: string | null;
108
+ recordedAt: string;
109
+ };
110
+ export declare function checkFactOf(store: Store, runId: number): CheckFact | null;
111
+ /** A check's recorded results, by source then identity. */
112
+ export declare function checkResultsOf(store: Store, runId: number): {
113
+ identity: string;
114
+ state: CheckResultState;
115
+ source: CheckResultSource;
116
+ }[];
117
+ /**
118
+ * What a project's Full checks are asked about for learning (`learn`): its tracked identities (learning_watch) — an
119
+ * accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
120
+ * most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
121
+ */
122
+ export declare function learnRequestFor(store: Store, repo: string, now: Date, runId?: number): {
123
+ learn: string[];
124
+ leftOut: number;
125
+ };
126
+ export {};
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Check facts (docs/plans/learning-loops.md, "Foundation: check facts"): what each settled check ran under and what it
3
+ * reported, recorded at disposition, never trusted for authority. The only writer of `check_fact` and `check_result`
4
+ * (the v149 migration aside); release-auto's learning scan (eval-hooks.ts learningWriteProblem) needs a person for
5
+ * SQL that writes them from anywhere else. A policy path (release-auto.ts EVAL_POLICY): any change needs a person.
6
+ *
7
+ * - Conditions are captured when the check's command starts (captureCheckConditions, called from the builder's
8
+ * runVerification), held in memory and passed here: the commit's tree read through verified objects (never the
9
+ * checkout), the approved command's digest, the check's access, the harness digest and lockfile, and the values of
10
+ * every environment variable the check inherits (the setup allowlist), hashed with checkContextOf's fields as
11
+ * `conditions_digest`. Not re-read later.
12
+ * - Results are tagged by source: `receipt` (the plane's own whole-check result: trusted for "the check passed or
13
+ * failed", nothing finer), `report` (the candidate-written report's lines), `log` (Vitest's FAIL lines in the sealed
14
+ * log, failures only, file level) and `learn` (the advisory learning line). Every failure is recorded; the check's
15
+ * blocking failure is the check's and is never changed here.
16
+ * - Explicit execution evidence is a `report` or `learn` line naming an identity. No line is no evidence: never
17
+ * skipped, removed or passed.
18
+ * - Fail closed: an unreadable tree, or conditions never captured (the command never started), records the fact row
19
+ * with no conditions and no results at all.
20
+ *
21
+ * Nothing here changes a check's outcome, a verdict, an approval or a rerun: every reader of a check's outcome reads
22
+ * the receipt and run_check, which this module never writes.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import { failingTestsOf } from "./gate-failure.js";
26
+ import { checkContextOf, projectGitDir, treeFactsOf } from "./check-report.js";
27
+ import { readVerifiedArtifact } from "./evidence.js";
28
+ import { recordObservations } from "./flaky.js";
29
+ import { considerLessonNote, markTalliesPartial, revisedFailures, tallyCheck } from "./lesson-notes.js";
30
+ import { CHECK_REPORT_PREFIX, isExactCase, learnLineSchema, learnRequest, LEARN_MAX_BYTES, LEARN_PREFIX } from "./contracts/release-check.js";
31
+ import { nightlyTaskRef, releaseTaskRef } from "./store.js";
32
+ /** The sources a result can come from, and the states it can have. */
33
+ export const CHECK_RESULT_SOURCES = Object.freeze(["receipt", "report", "log", "learn"]);
34
+ /** The receipt's whole-check identity, and the one a failed check with nothing else named records. */
35
+ export const WHOLE_CHECK = "check";
36
+ export const UNEXPLAINED = "unexplained";
37
+ /** How many result rows one check records at most (a report's own bounds are far above any real check). */
38
+ export const MAX_RESULTS_PER_CHECK = 20_000;
39
+ /** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
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;
43
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
44
+ /**
45
+ * The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
46
+ * access, and the values of `envNames` (the variables the check inherits, builder.ts SETUP_ENV_ALLOWLIST) as this
47
+ * process holds them now — what the command is given. Hashed with checkContextOf's fields; the class reads
48
+ * `<platform>-<arch>-node<major>`. Values are hashed, never stored.
49
+ */
50
+ export function captureCheckConditions(input) {
51
+ const none = (problem) => ({ head: input.head, tree: null, commandDigest: input.command.digest, access: input.access, harnessDigest: null, lockfile: null, conditionsDigest: null, conditionsClass: null, problem });
52
+ if (input.repo === null)
53
+ return none("the check's project isn't recorded");
54
+ const gitDir = projectGitDir(input.repo);
55
+ if (gitDir === null)
56
+ return none("the project's Git directory can't be read");
57
+ let read;
58
+ // Advisory: whatever happens reading the tree, the check itself is never stopped by it.
59
+ try {
60
+ read = treeFactsOf(gitDir, input.head);
61
+ }
62
+ catch (error) {
63
+ return none(`the checked commit can't be read from verified objects (${error instanceof Error ? error.message : String(error)})`);
64
+ }
65
+ if (!read.ok)
66
+ return none(`the checked commit can't be read from verified objects (${read.problem})`);
67
+ const context = checkContextOf(read.facts, input.command, input.access);
68
+ const env = input.env ?? process.env;
69
+ const environment = Object.fromEntries([...new Set(input.envNames)].sort().map(name => [name, env[name] ?? null]));
70
+ const digest = sha256(JSON.stringify({ version: 1, context, environment }));
71
+ const major = /^v?(\d+)/.exec(context.node)?.[1] ?? "unknown";
72
+ return { head: input.head, tree: read.facts.tree, commandDigest: input.command.digest, access: input.access, harnessDigest: read.facts.harnessDigest, lockfile: read.facts.lockfile,
73
+ conditionsDigest: digest, conditionsClass: `${context.platform}-${context.arch}-node${major}`, problem: null };
74
+ }
75
+ /** Why the task's check ran: a release's or the nightly's purpose, read from the plane's own records. */
76
+ export function checkPurposeOf(store, taskRef) {
77
+ if (nightlyTaskRef(store, taskRef))
78
+ return "nightly";
79
+ if (releaseTaskRef(store, taskRef))
80
+ return "release";
81
+ const terms = store.handle.prepare(`SELECT s.release_terms_json, s.release_check_json FROM task_scope s JOIN task_ref r ON r.external_id = s.task_id AND r.backend = 'built-in' WHERE r.id = ?`).get(taskRef);
82
+ return terms !== undefined && (terms["release_terms_json"] != null || terms["release_check_json"] != null) ? "release" : "ordinary";
83
+ }
84
+ /** The advisory learning line of a check's output, read strictly: exactly one line carrying this run's nonce, at most
85
+ * 64 KiB, before the report, matching its contract, naming only requested exact cases, each once. Anything else is
86
+ * not recorded, with why; that loses learning evidence only. */
87
+ export function learningLinesOf(stdout, nonce, asked) {
88
+ const lines = stdout.split("\n");
89
+ let reportAt = -1;
90
+ for (let at = lines.length - 1; at >= 0; at--)
91
+ if (lines[at].startsWith(CHECK_REPORT_PREFIX)) {
92
+ reportAt = at;
93
+ break;
94
+ }
95
+ const ours = [];
96
+ let oversized = false, others = false, unreadable = false;
97
+ lines.forEach((line, at) => {
98
+ if (!line.startsWith(LEARN_PREFIX))
99
+ return;
100
+ if (Buffer.byteLength(line) > LEARN_MAX_BYTES) {
101
+ oversized = true;
102
+ return;
103
+ }
104
+ let value;
105
+ try {
106
+ value = JSON.parse(line.slice(LEARN_PREFIX.length));
107
+ }
108
+ catch {
109
+ unreadable = true;
110
+ return;
111
+ }
112
+ if (value === null || typeof value !== "object" || value["nonce"] !== nonce) {
113
+ others = true;
114
+ return;
115
+ }
116
+ ours.push({ at, value });
117
+ });
118
+ if (ours.length === 0)
119
+ return { ok: false, why: oversized ? "the learning line was over 64 KiB" : unreadable ? "the learning line couldn't be read" : others ? "no learning line carried this run's nonce" : "the check printed no learning line" };
120
+ if (ours.length > 1)
121
+ return { ok: false, why: "the check printed more than one learning line" };
122
+ if (oversized)
123
+ return { ok: false, why: "a learning line was over 64 KiB" };
124
+ const only = ours[0];
125
+ if (reportAt >= 0 && only.at > reportAt)
126
+ return { ok: false, why: "the learning line came after the report" };
127
+ const read = learnLineSchema.safeParse(only.value);
128
+ if (!read.success)
129
+ return { ok: false, why: "the learning line doesn't match its contract" };
130
+ const wanted = new Set(asked);
131
+ const seen = new Set();
132
+ for (const one of read.data.results) {
133
+ if (!wanted.has(one.identity) || !isExactCase(one.identity))
134
+ return { ok: false, why: "the learning line named an identity it wasn't asked about" };
135
+ if (seen.has(one.identity))
136
+ return { ok: false, why: "the learning line named an identity twice" };
137
+ seen.add(one.identity);
138
+ }
139
+ return { ok: true, results: read.data.results };
140
+ }
141
+ const RANK = { passed: 0, skipped: 1, "passed-on-retry": 2, failed: 3 };
142
+ /** The report's lines as results: every case it names; a file, group, version or part only when it didn't simply pass. */
143
+ export function reportResults(report) {
144
+ const rows = [];
145
+ const add = (identity, state) => rows.push({ identity, state, source: "report" });
146
+ for (const one of report.units?.identities ?? [])
147
+ add(`unit:${one.file}::${one.name}`, one.state === "passed" && one.flaky ? "passed-on-retry" : one.state);
148
+ for (const one of report.units?.files ?? []) {
149
+ if (one.state !== "passed")
150
+ add(`file:${one.file}`, one.state);
151
+ else if (one.flaky)
152
+ add(`file:${one.file}`, "passed-on-retry");
153
+ }
154
+ for (const run of report.journeys)
155
+ for (const group of run.groups) {
156
+ if (group.state === "failed")
157
+ add(`group:${run.script}:${run.kind}:${group.group}`, "failed");
158
+ else if (group.state === "passed" && group.retried)
159
+ add(`group:${run.script}:${run.kind}:${group.group}`, "passed-on-retry");
160
+ if (group.state === "not-run")
161
+ continue;
162
+ for (const journey of group.journeys)
163
+ add(`journey:${run.script}:${run.kind}:${group.group}::${journey.name}`, journey.state === "passed" && group.retried ? "passed-on-retry" : journey.state);
164
+ }
165
+ for (const one of report.upgrades?.versions ?? [])
166
+ if (one.state === "failed")
167
+ add(`upgrade:${one.version}`, "failed");
168
+ for (const part of report.parts)
169
+ if (part.selected && part.ran && (part.code !== 0 || part.timedOut))
170
+ add(`part:${part.part}`, "failed");
171
+ return rows;
172
+ }
173
+ /** The sealed log's FAIL lines (Vitest), failures only, file level; nothing when the log's own count disagrees. */
174
+ function logResults(store, root, runId) {
175
+ const logs = store.artifactsFor(runId).filter(one => one.kind === "check-log");
176
+ if (logs.length !== 1)
177
+ return [];
178
+ const read = readVerifiedArtifact(root, logs[0]);
179
+ if (!read.ok)
180
+ return [];
181
+ const failing = failingTestsOf(read.content.toString("utf8")) ?? [];
182
+ return failing.filter(one => /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))[^\u0000-\u001f\\]{1,512}$/.test(one.file)).map(one => ({ identity: `file:${one.file}`, state: "failed", source: "log" }));
183
+ }
184
+ /**
185
+ * Record what a settled check ran under and reported, once (first write wins), in one transaction: its fact row and
186
+ * every result row. Called at disposition by the builder right after it records the check's own outcome. Advisory:
187
+ * nothing reads these for a verdict, a rerun or an approval.
188
+ */
189
+ export function recordCheckFacts(store, root, input) {
190
+ const conditions = input.conditions;
191
+ const comparable = conditions !== null && conditions.tree !== null && conditions.conditionsDigest !== null;
192
+ const asked = input.attempt === null ? [] : learnRequest(input.attempt.learn);
193
+ let learnState = asked.length === 0 ? "not-asked" : "not-recorded";
194
+ let learnWhy = null;
195
+ const rows = [];
196
+ if (comparable) {
197
+ if (input.receipt.status !== "not-run")
198
+ rows.push({ identity: WHOLE_CHECK, state: input.receipt.status, source: "receipt" });
199
+ if (input.report !== null)
200
+ rows.push(...reportResults(input.report));
201
+ rows.push(...logResults(store, root, input.runId));
202
+ }
203
+ if (asked.length > 0) {
204
+ if (!comparable)
205
+ learnWhy = conditions === null ? "the check's command never started" : conditions.problem ?? "the check's conditions weren't recorded";
206
+ else if (input.stdout === null || input.attempt === null)
207
+ learnWhy = "the check's output wasn't kept";
208
+ else {
209
+ const learned = learningLinesOf(input.stdout, input.attempt.nonce, asked);
210
+ if (!learned.ok)
211
+ learnWhy = learned.why;
212
+ else {
213
+ learnState = "recorded";
214
+ for (const one of learned.results)
215
+ rows.push({ identity: one.identity, state: one.state, source: "learn" });
216
+ }
217
+ }
218
+ }
219
+ if (comparable && input.receipt.status === "failed" && !rows.some(one => one.source !== "receipt" && one.state === "failed"))
220
+ rows.push({ identity: UNEXPLAINED, state: "failed", source: "receipt" });
221
+ // One row per (identity, source), its worst state; bounded.
222
+ const kept = new Map();
223
+ for (const row of rows) {
224
+ if (row.identity.length < 1 || row.identity.length > 2600)
225
+ continue;
226
+ const key = `${row.source}\u0000${row.identity}`;
227
+ const before = kept.get(key);
228
+ if (before === undefined || RANK[row.state] > RANK[before.state])
229
+ kept.set(key, row);
230
+ }
231
+ const at = input.now.toISOString();
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,
236
+ conditions_class, purpose, learn_state, learn_why, recorded_at) SELECT id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM run WHERE id = ?
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);
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);
273
+ }
274
+ export function checkFactOf(store, runId) {
275
+ const row = store.handle.prepare("SELECT * FROM check_fact WHERE run = ?").get(runId);
276
+ if (row === undefined)
277
+ return null;
278
+ const text = (key) => row[key] === null ? null : String(row[key]);
279
+ return { run: Number(row["run"]), repo: text("repo"), taskRef: Number(row["task_ref"]), head: text("head"), tree: text("tree"), commandDigest: text("command_digest"), access: text("access"),
280
+ harnessDigest: text("harness_digest"), lockfile: text("lockfile"), conditionsDigest: text("conditions_digest"), conditionsClass: text("conditions_class"),
281
+ purpose: String(row["purpose"]), learnState: String(row["learn_state"]), learnWhy: text("learn_why"), recordedAt: String(row["recorded_at"]) };
282
+ }
283
+ /** A check's recorded results, by source then identity. */
284
+ export function checkResultsOf(store, runId) {
285
+ return store.handle.prepare("SELECT identity, state, source FROM check_result WHERE run = ? ORDER BY source, identity").all(runId)
286
+ .map(row => ({ identity: String(row["identity"]), state: String(row["state"]), source: String(row["source"]) }));
287
+ }
288
+ /**
289
+ * What a project's Full checks are asked about for learning (`learn`): its tracked identities (learning_watch) — an
290
+ * accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
291
+ * most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
292
+ */
293
+ export function learnRequestFor(store, repo, now, runId) {
294
+ const since = new Date(now.getTime() - OBSERVATION_WATCH_MS).toISOString();
295
+ let tracked;
296
+ // Advisory: a read that fails asks about nothing, and the check runs as it would have.
297
+ try {
298
+ tracked = store.handle.prepare(`SELECT identity, MAX(since) AS latest FROM learning_watch
299
+ WHERE repo = ? AND ((reason = 'note' AND until IS NULL) OR (reason = 'observation' AND since >= ?))
300
+ GROUP BY identity ORDER BY latest DESC, identity`).all(repo, since).map(row => String(row["identity"])).filter(isExactCase);
301
+ }
302
+ catch {
303
+ return { learn: [], leftOut: 0 };
304
+ }
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 };
318
+ }