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
package/dist/dispatch.js CHANGED
@@ -16,6 +16,8 @@ import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js"
16
16
  import { replacedWords } from "./task-status.js";
17
17
  import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
18
18
  import { buildReviewOf } from "./review-switch.js";
19
+ import { vanishedStopOf, vanishedStopSentence } from "./vanished-retry.js";
20
+ import { routeChangeOf } from "./route-change.js";
19
21
  import { BUILT_IN, refArchived, parseCapabilityKey } from "./store.js";
20
22
  import { shortWhen } from "./when-html.js";
21
23
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
@@ -81,7 +83,7 @@ export function scopeApprovedForDispatch(store, taskRef, now) {
81
83
  JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
82
84
  WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL
83
85
  AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
84
- .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
86
+ .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString(), now.toISOString());
85
87
  return row !== undefined;
86
88
  }
87
89
  /** The phase the ordinary dispatch loop would choose before it resolves an
@@ -229,9 +231,9 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
229
231
  return answer(code, code === "reviewing" ? "running" : "waiting", status.label, status.detail, { action: retry.state === "retryable" ? "retry-review" : "open-result", review });
230
232
  }
231
233
  if (!accepted && proof?.verdict === "refuted")
232
- return answer("proof-refuted", "waiting", status.label, `${status.detail} Review the evidence to correct it, or record an explicit acceptance.`, { action: "open-result", review });
234
+ return answer("proof-refuted", "waiting", status.label, `${status.detail} Open the result to send it back, or accept it with a reason.`, { action: "open-result", review });
233
235
  if (!accepted && proof?.verdict === "short")
234
- return answer("needs-verification", "waiting", status.label, `${status.detail} Open the result for the exact criteria still needing verification.`, { action: "open-result", review });
236
+ return answer("needs-verification", "waiting", status.label, `${status.detail} Open the result to see which requirements still need checking.`, { action: "open-result", review });
235
237
  if (!accepted && proof === null && noChangeRecord !== true)
236
238
  return answer("needs-verification", "waiting", status.label, status.detail, { action: "open-result", review });
237
239
  return answer("complete", "terminal", status.label, review?.state === "succeeded" && review.attempt !== null && review.attempt > 1
@@ -248,12 +250,13 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
248
250
  // The family's latest finished attempt's own recorded reason, or that none was recorded: never "review its incident".
249
251
  const family = store.taskFamilyOf(taskId, ref.repo === null ? [] : [ref.repo], ref.repo === null);
250
252
  const runs = family === null || family.problem !== null ? store.runsFor(ref.id) : family.versions.flatMap(version => store.runsFor(version.refId));
251
- return answer("failed", "terminal", "Needs a retry", failedAttemptSentence(latestFinishedAttempt(runs)?.reason), { action: "retry-task" });
253
+ const stopped = vanishedStopOf(store, ref.id);
254
+ return answer("failed", "terminal", "Needs a retry", stopped === null ? failedAttemptSentence(latestFinishedAttempt(runs)?.reason) : vanishedStopSentence(stopped), { action: "retry-task" });
252
255
  }
253
256
  if (store.hasLiveClaim(ref.id, now))
254
- return answer("running", "running", "Running now", "A worker owns the current live claim.");
257
+ return answer("running", "running", "Running now", "A builder is working on it now.");
255
258
  if (task.state === "running")
256
- return answer("vanished-run", "waiting", "Build vanished", "The task says running, but no current claim owns it; reconcile it before retrying.", { action: "retry-task" });
259
+ return answer("vanished-run", "waiting", "Build vanished", "The build stopped unexpectedly. Retry it.", { action: "retry-task" });
257
260
  if (updateAdmissionPaused(store.raw()))
258
261
  return answer("updating", "waiting", "Waiting for app update", UPDATE_PAUSED);
259
262
  const local = taskReadinessBlocker(store, ref.id, now);
@@ -270,7 +273,7 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
270
273
  if (local.ownerKind === "decision")
271
274
  return answer("waiting-decision", "waiting", "Waiting on your answer", "An agent parked a question; answering it resumes the task.", { action: "answer-decision" });
272
275
  if (local.ownerKind === "incident")
273
- return answer("waiting-incident", "waiting", "Needs a retry", "An unresolved incident holds the next attempt.", { action: "retry-task" });
276
+ return answer("waiting-incident", "waiting", "Needs a retry", "The last attempts failed the same way. Retry, or change the plan.", { action: "retry-task" });
274
277
  if (local.ownerKind === "stop")
275
278
  return answer("stopped", "waiting", "Paused", `${local.message.replace(/^held:\s*/, "")}. The attempt's work is preserved; resuming that exact attempt lifts this pause.`, { action: "resume-run" });
276
279
  return answer("held", "waiting", "On hold", local.message.replace(/^held:\s*/, ""), { action: local.ownerKind === "operator" ? "unhold" : "inspect-hold" });
@@ -288,14 +291,14 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
288
291
  const role = dispatchRoleFor(store, ref.id, now);
289
292
  const scope = store.getScope(taskId);
290
293
  if (ref.repo === null)
291
- return answer("needs-project", "waiting", "Needs a project", "Place this task in a repository before a worker can claim it.", { action: "place-task", role });
294
+ return answer("needs-project", "waiting", "Needs a project", "Choose a project for this task so a builder can start it.", { action: "place-task", role });
292
295
  if (role !== "planner") {
293
296
  if (scope === null)
294
- return answer("needs-scope", "waiting", "Needs a scope", "Write the success contract or ask the planner to draft it.", { action: "write-scope", role });
297
+ return answer("needs-scope", "waiting", "Needs a plan", "Write the plan, or ask the planner to draft it.", { action: "write-scope", role });
295
298
  if (scope.profileState === "unresolved")
296
299
  return answer("needs-agent-profile", "waiting", "Needs an agent profile", scope.unresolvedReason ?? "Choose an available provider and model.", { action: "select-agent", role });
297
300
  if (!scopeApprovedForDispatch(store, ref.id, now) || !approvalOf(scope).approved) {
298
- return answer("needs-approval", "waiting", "Needs your approval", "Review and sign the current scope before a worker can claim it.", { action: "approve-scope", role });
301
+ return answer("needs-approval", "waiting", "Needs your approval", routeChangeOf(store, taskId) ?? "Review the plan and approve it so a builder can start.", { action: "approve-scope", role });
299
302
  }
300
303
  }
301
304
  const gap = missingCapability(store, ref.id, ref.repo, now);
@@ -315,14 +318,14 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
315
318
  }
316
319
  const available = alive.filter(one => store.liveClaimCount(one.name, now) < one.capacity);
317
320
  if (available.length === 0)
318
- return answer("worker-at-capacity", "retrying", "Waiting for worker capacity", "Every eligible worker is busy; this task starts when a slot is released.", { role });
321
+ return answer("worker-at-capacity", "retrying", "Waiting its turn", "Builders are busy; it starts when one is free.", { role });
319
322
  const profile = approvedProfile(scope);
320
323
  if (profile !== null) {
321
324
  const exhausted = available.map(one => store.readQuotaState(one.name, profile.provider, profile.model, now)).filter(one => one?.state === "exhausted");
322
325
  if (exhausted.length === available.length) {
323
326
  const resets = exhausted.map(one => one?.resetAt ?? null).filter((one) => one !== null).sort();
324
327
  const nextAt = resets[0] ?? null;
325
- return answer("provider-quota", "retrying", "Provider quota exhausted", nextAt === null ? "The provider has not supplied a reset time; this needs operator attention." : `The next recorded reset is ${nextAt}.`, { action: nextAt === null ? "select-agent" : null, nextAt, role });
328
+ return answer("provider-quota", "retrying", "Provider quota exhausted", nextAt === null ? "The provider gave no reset time. Choose another agent, or wait for the limit to reset." : `The next recorded reset is ${nextAt}.`, { action: nextAt === null ? "select-agent" : null, nextAt, role });
326
329
  }
327
330
  }
328
331
  // The sign-in pause: the task's provider stopped working; nothing it would
@@ -343,11 +346,11 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
343
346
  const sourceProblem = plannerSourceProblemOf(store, taskId);
344
347
  if (sourceProblem !== null)
345
348
  return answer("planner-source", "waiting", sourceProblem.title, sourceProblem.detail, { action: "write-scope", role });
346
- return answer("planning-ready", "retrying", "Planner ready", "An eligible worker can draft the scope on the next pass.", { role });
349
+ return answer("planning-ready", "retrying", "Waiting to plan", "A builder can draft the plan next.", { role });
347
350
  }
348
351
  if (role === "scout")
349
- return answer("scouting-ready", "retrying", "Scout ready", "An eligible worker can produce the report on the next pass.", { role });
350
- return answer("ready", "retrying", "Ready to run", "An eligible worker has capacity and every dispatch gate currently passes.", { role });
352
+ return answer("scouting-ready", "retrying", "Waiting to research", "A builder can write the report next.", { role });
353
+ return answer("ready", "retrying", "Ready to run", "A builder is free and nothing is in the way.", { role });
351
354
  }
352
355
  export function diagnosisIsDispatchable(diagnosis) {
353
356
  return diagnosis?.code === "ready" || diagnosis?.code === "planning-ready" || diagnosis?.code === "scouting-ready";
package/dist/dispose.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // Assignment tracking grants no repair authority or attempts.
2
+ import { authorizeCheckOnly } from "./check-only.js";
3
+ import { proposeNewRoute } from "./route-change.js";
2
4
  import { originalTaskBase, focusedTestCommandSupported } from "./observations.js";
3
5
  /**
4
6
  * The build disposition service (Parity II Phase 2, v4 Q2 / v6 W1): the
@@ -158,6 +160,8 @@ function disposeBuildOutcomeLocked(context, result) {
158
160
  });
159
161
  // A stated no-change publishes nothing.
160
162
  if (result.noChange !== true && result.committed) {
163
+ // The approval that built this commit allows one automatic recheck of it, should its verdict go missing.
164
+ authorizeCheckOnly(store, runId, clock());
161
165
  const grant = store.publicationGrantFor(repo);
162
166
  const headSha = store.getRun(runId)?.headRevision ?? null;
163
167
  if (grant !== null &&
@@ -357,6 +361,8 @@ function disposeBuildOutcomeLocked(context, result) {
357
361
  */
358
362
  export function holdStaleApproval(store, args, now) {
359
363
  const scope = store.getScope(args.taskId);
364
+ // Toolroll proposes the new exact route itself (route-change.ts): the person's one act is Approve, on one line.
365
+ const proposed = proposeNewRoute(store, args.taskRef, args.taskId, now);
360
366
  store.holdOwned({
361
367
  taskRef: args.taskRef,
362
368
  ownerKind: "backoff",
@@ -370,8 +376,8 @@ export function holdStaleApproval(store, args, now) {
370
376
  kind: "stale-approval",
371
377
  pushClass: "attention",
372
378
  link: `/t/${encodeURIComponent(args.taskId)}`,
373
- subject: `${args.taskId}: its approval no longer matches the build routing — approve it again`,
374
- body: `${args.message}\nNothing runs on it until the scope is approved again; the hold lifts with the yes.`,
379
+ subject: `${args.taskId}: its agents changed — approve it again`,
380
+ body: `${proposed?.words ?? args.message}\nNothing runs on it until it's approved again.`,
375
381
  }, now);
376
382
  }
377
383
  // ---- the bounded repair loop (v40, evidence-review-v1) --------------------
@@ -284,9 +284,8 @@ export function defaultEasyStartSeams(options) {
284
284
  }
285
285
  },
286
286
  findRepo: async (dir) => {
287
- const { run } = await import("./exec.js");
288
- const top = await run("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeoutMs: 10_000 });
289
- return top.code === 0 ? top.stdout.trim() : null;
287
+ const { checkoutTop } = await import("./teardown-git.js");
288
+ return checkoutTop(dir);
290
289
  },
291
290
  enrolled: async (repo) => {
292
291
  const { loadRepos } = await import("./repos.js");
@@ -1,3 +1,4 @@
1
+ import type ts from "typescript-parser";
1
2
  /** One required call: in this file, inside this top-level function, a call to this name, the name bound as `from`
2
3
  * says (a module specifier its un-renamed named import comes from; `self`: the file's own top-level function; `local`:
3
4
  * declared inside `inside`). `args`: the call's arguments, as text, must stay as the base has them. */
@@ -9,12 +10,17 @@ export type EvalHook = {
9
10
  args?: true;
10
11
  why: string;
11
12
  };
13
+ /** `fn` may also be `outer>inner` (docs/plans/learning-loops.md, Protection 3): a `const inner = (…) => …` (or function
14
+ * expression) or `function inner`, bound once in the file and never reassigned, inside the top-level function `outer`
15
+ * and in no other function between them. `strict`: a base that holds the file but can't resolve the pin is a problem,
16
+ * never skipped (a base without the file at all is a project that isn't Toolroll, and holds none). */
12
17
  /** One function whose body (its text, as the candidate has it) must stay as the base has it: a top-level function, or
13
18
  * `Class#method`, a method of a top-level class. */
14
19
  export type EvalPinned = {
15
20
  file: string;
16
21
  fn: string;
17
22
  why: string;
23
+ strict?: true;
18
24
  };
19
25
  export declare const EVAL_HOOKS: readonly EvalHook[];
20
26
  export declare const EVAL_PINNED: readonly EvalPinned[];
@@ -37,3 +43,52 @@ export declare const EVAL_HOOK_FILES: readonly string[];
37
43
  * files the verified diff says exist at the candidate, so no bytes is a failed read, never a deletion.
38
44
  */
39
45
  export declare function storeRewriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null, pinned?: readonly string[]): string | null;
46
+ /** The calls that record a check's facts: at disposition, and as the check's command starts. */
47
+ export declare const LEARNING_HOOKS: readonly EvalHook[];
48
+ /** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
49
+ * proposal and a decision are written, and where a check's conditions are captured. */
50
+ export declare const LEARNING_PINNED: readonly EvalPinned[];
51
+ /** Every file the learning manifest reads. */
52
+ export declare const LEARNING_HOOK_FILES: readonly string[];
53
+ /** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
54
+ * existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
55
+ export declare const LEARNING_WRITERS: Readonly<Record<string, readonly string[]>>;
56
+ /** One SQL-shaped construct in a file: its static text (a placeholder `${}` where an expression sits), the protected
57
+ * tables it writes, and the top-level declaration it's in ("" for none). `unreadable`: a write whose table name is
58
+ * split around an expression; `bare`: a string that is only a protected table's name (SQL assembled elsewhere). */
59
+ type LearningConstruct = {
60
+ text: string;
61
+ tables: string[];
62
+ inside: string;
63
+ unreadable: boolean;
64
+ bare: boolean;
65
+ };
66
+ /**
67
+ * Every construct in the file that could write a protected table: each string literal, template (its static parts)
68
+ * and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
69
+ * helper. A write verb next to a protected table's name, a table name split around an expression next to a write
70
+ * word, and a string that is only a protected table's name are each found.
71
+ */
72
+ export declare function learningConstructs(source: ts.SourceFile): LearningConstruct[];
73
+ /**
74
+ * Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
75
+ * (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
76
+ * construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
77
+ * (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
78
+ * text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
79
+ */
80
+ export declare function learningWriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null): string | null;
81
+ /**
82
+ * Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
83
+ * pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
84
+ * the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
85
+ */
86
+ export declare function learningHookProblem(manifest: {
87
+ candidate: (file: string) => Buffer | null;
88
+ base?: (file: string) => Buffer | null;
89
+ }, changed: {
90
+ paths: readonly string[];
91
+ candidate: ((file: string) => Buffer | null) | null;
92
+ base: ((file: string) => Buffer | null) | null;
93
+ }): string | null;
94
+ export {};
@@ -36,6 +36,7 @@ export const EVAL_HOOKS = Object.freeze([
36
36
  { file: "src/release-auto.ts", inside: "changeProblem", calls: "evalHookProblem", from: "./eval-hooks.js", why: "this check itself" },
37
37
  { file: "src/release-auto.ts", inside: "changeProblem", calls: "autoPathProblem", from: "self", why: "policy paths need a person" },
38
38
  { file: "src/release-auto.ts", inside: "changeProblem", calls: "storeRewriteProblem", from: "./eval-hooks.js", why: "no changed file rewrites a pinned Store method" },
39
+ { file: "src/release-auto.ts", inside: "changeProblem", calls: "learningHookProblem", from: "./eval-hooks.js", why: "learning loops' hooks, pins and writers (docs/plans/learning-loops.md)" },
39
40
  // The invocation gateway (invoke.ts): every agent run an eval world spawns takes a slot.
40
41
  { file: "src/invoke.ts", inside: "invokeAgent", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every agent dispatch in an eval world takes a slot" },
41
42
  // PR #263 review: what the run's billing basis and its unknown cost are computed from is held too, not only the call.
@@ -227,7 +228,7 @@ function boundAs(source, name, from, inside) {
227
228
  if (from === "self")
228
229
  return compiler.isFunctionDeclaration(only) && only.parent === source ? null : `${name} isn't its own top-level function`;
229
230
  if (from === "local")
230
- return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside ${inside.name?.text ?? "it"}`;
231
+ return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside its function`;
231
232
  if (!compiler.isImportSpecifier(only) || only.propertyName !== undefined || only.isTypeOnly)
232
233
  return `${name} isn't imported by its own name`;
233
234
  const clause = only.parent.parent;
@@ -254,6 +255,46 @@ function heldFunction(source, name) {
254
255
  const fn = topFunction(source, name);
255
256
  return fn === null || boundAs(source, name, "self", fn) !== null ? null : fn;
256
257
  }
258
+ /** `outer>inner`: the one function-like `inner` declared inside the held top-level `outer` (no other function between
259
+ * them), bound once in the file and never reassigned — a `function inner` or a `const inner =` arrow or function
260
+ * expression. Null otherwise. */
261
+ function nestedFunction(source, spec) {
262
+ const compiler = typescript();
263
+ const [outerName, innerName, ...rest] = spec.split(">");
264
+ if (outerName === undefined || innerName === undefined || rest.length > 0 || outerName === "" || innerName === "")
265
+ return null;
266
+ const outer = heldFunction(source, outerName);
267
+ if (outer === null || outer.body === undefined)
268
+ return null;
269
+ const all = bindingsOf(source, innerName);
270
+ if (all.length !== 1)
271
+ return null;
272
+ const only = all[0];
273
+ let fn = null;
274
+ if (compiler.isFunctionDeclaration(only) && only.body !== undefined)
275
+ fn = only;
276
+ else if (compiler.isVariableDeclaration(only) && only.initializer !== undefined && (compiler.isArrowFunction(only.initializer) || compiler.isFunctionExpression(only.initializer))
277
+ && compiler.isVariableDeclarationList(only.parent) && (only.parent.flags & compiler.NodeFlags.Const) !== 0)
278
+ fn = only.initializer;
279
+ if (fn === null)
280
+ return null;
281
+ // Directly inside `outer`: the first function-like ancestor of the declaration is `outer` itself.
282
+ for (let at = only.parent; at !== undefined; at = at.parent) {
283
+ if (at === outer)
284
+ return fn;
285
+ if (compiler.isFunctionLike(at) || compiler.isClassLike(at))
286
+ return null;
287
+ }
288
+ return null;
289
+ }
290
+ /** A held function by its manifest name: top-level, `Class#method` or `outer>inner`. */
291
+ function heldNode(source, spec) {
292
+ if (spec.includes(">"))
293
+ return nestedFunction(source, spec);
294
+ if (spec.includes("#"))
295
+ return classMethod(source, spec);
296
+ return heldFunction(source, spec);
297
+ }
257
298
  /** A reader that parses each file once. */
258
299
  function parser(read) {
259
300
  const seen = new Map();
@@ -265,7 +306,7 @@ function hookIn(tree, hook) {
265
306
  const parsed = tree(hook.file);
266
307
  if ("problem" in parsed)
267
308
  return { missing: `${parsed.problem}, and it holds a required call (${hook.why})` };
268
- const fn = heldFunction(parsed.source, hook.inside);
309
+ const fn = (hook.inside.includes(">") ? nestedFunction(parsed.source, hook.inside) : heldFunction(parsed.source, hook.inside));
269
310
  if (fn === null || fn.body === undefined)
270
311
  return { missing: `${hook.file} no longer has ${hook.inside} as one function, which must call ${hook.calls} (${hook.why})` };
271
312
  const found = callsOf(fn.body, hook.calls);
@@ -281,7 +322,7 @@ function pinnedText(tree, pin) {
281
322
  const parsed = tree(pin.file);
282
323
  if ("problem" in parsed)
283
324
  return null;
284
- const fn = pin.fn.includes("#") ? classMethod(parsed.source, pin.fn) : heldFunction(parsed.source, pin.fn);
325
+ const fn = heldNode(parsed.source, pin.fn);
285
326
  if (fn === null)
286
327
  return null;
287
328
  // The static block's pin holds the class header too: its name, modifiers and heritage, as the base has them.
@@ -308,8 +349,12 @@ export function evalHookProblem(candidate, base, hooks = EVAL_HOOKS, pinned = EV
308
349
  }
309
350
  for (const pin of pinned) {
310
351
  const before = was === null ? null : pinnedText(was, pin);
311
- if (was !== null && before === null)
352
+ if (was !== null && before === null) {
353
+ // A strict pin the base holds the file of but can't resolve is a problem (learning loops, Protection 2), never a skip.
354
+ if (pin.strict === true && base(pin.file) !== null)
355
+ return `${pin.file} at the base has no ${pin.fn} as one function, which is pinned strictly (${pin.why})`;
312
356
  continue;
357
+ }
313
358
  const after = pinnedText(now, pin);
314
359
  if (after === null)
315
360
  return `${pin.file} no longer has ${pin.fn} as one function (${pin.why})`;
@@ -475,3 +520,181 @@ export function storeRewriteProblem(paths, candidate, base, pinned = PINNED_STOR
475
520
  }
476
521
  return null;
477
522
  }
523
+ // ---- learning loops (docs/plans/learning-loops.md, "Safety", Protection 2-4) ----------------------------------------
524
+ /** The calls that record a check's facts: at disposition, and as the check's command starts. */
525
+ export const LEARNING_HOOKS = Object.freeze([
526
+ { file: "src/builder.ts", inside: "settleProof", calls: "recordCheckFacts", from: "./check-facts.js", why: "every settled check records its facts" },
527
+ { file: "src/builder.ts", inside: "settleProof>runVerification", calls: "captureCheckConditions", from: "./check-facts.js", why: "a check's conditions are captured as its command starts" },
528
+ ]);
529
+ /** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
530
+ * proposal and a decision are written, and where a check's conditions are captured. */
531
+ export const LEARNING_PINNED = Object.freeze([
532
+ { file: "src/project-knowledge.ts", fn: "knowledgeContext", strict: true, why: "the decisions and references a run is given, and their snapshot" },
533
+ { file: "src/project-learning.ts", fn: "learningContext", strict: true, why: "the lessons a run is given, and their snapshot" },
534
+ { file: "src/memory-pass.ts", fn: "decideProposal", strict: true, why: "a memory proposal's one writer" },
535
+ { file: "src/project-memory.ts", fn: "recordDecision", strict: true, why: "how a decision is recorded and what it supersedes" },
536
+ { file: "src/project-memory.ts", fn: "retireDecision", strict: true, why: "how a decision is retired" },
537
+ { file: "src/builder.ts", fn: "settleProof>runVerification", strict: true, why: "the check's command and the conditions it starts under" },
538
+ ]);
539
+ /** Every file the learning manifest reads. */
540
+ export const LEARNING_HOOK_FILES = Object.freeze([...new Set([...LEARNING_HOOKS.map(hook => hook.file), ...LEARNING_PINNED.map(pin => pin.file)])]);
541
+ /** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
542
+ * existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
543
+ export const LEARNING_WRITERS = Object.freeze({
544
+ check_fact: ["src/check-facts.ts"], check_result: ["src/check-facts.ts"],
545
+ local_observation: ["src/flaky.ts"], ci_flaky_observation: ["src/flaky-ci.ts"], ci_pairing_rejected: ["src/flaky-ci.ts"],
546
+ learning_watch: ["src/flaky.ts", "src/lesson-notes.ts"],
547
+ lesson_note: ["src/lesson-notes.ts"], lesson_note_tally: ["src/lesson-notes.ts"],
548
+ memory_use: ["src/memory-use.ts"],
549
+ memory_tidy_proposal: ["src/memory-tidy.ts"], decision_merge: ["src/memory-tidy.ts"],
550
+ project_decision: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
551
+ decision_change: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
552
+ });
553
+ /** store.ts's v149 migration: the only place outside the writers that may name these tables in SQL. */
554
+ const LEARNING_MIGRATION = Object.freeze({ "src/store.ts": ["V149_SCHEMA", "V149_TABLES", "V149_REPO_TABLES"] });
555
+ const PROTECTED = Object.keys(LEARNING_WRITERS);
556
+ const tableWrite = (table) => new RegExp(String.raw `(?:\b(?:(?:INSERT(?:\s+OR\s+\w+)?|REPLACE)\s+INTO|UPDATE(?:\s+OR\s+\w+)?|DELETE\s+FROM)\s+(?:main\s*\.\s*)?["\`\[]?${table}["\`\]]?(?!\w)`
557
+ + String.raw `|\b(?:DROP\s+(?:TABLE|TRIGGER|INDEX|VIEW)(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE)\s+(?:main\s*\.\s*)?["\`\[]?${table})`, "i");
558
+ const TABLE_WRITES = PROTECTED.map(table => ({ table, write: tableWrite(table) }));
559
+ const WRITE_WORD = /\b(?:INSERT|UPDATE|DELETE|REPLACE|DROP|ALTER)\b/i;
560
+ const BARE_NAME = new RegExp(String.raw `^\s*(?:main\s*\.\s*)?["\`\[]?(?:${PROTECTED.join("|")})["\`\]]?\s*$`, "i");
561
+ /**
562
+ * Every construct in the file that could write a protected table: each string literal, template (its static parts)
563
+ * and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
564
+ * helper. A write verb next to a protected table's name, a table name split around an expression next to a write
565
+ * word, and a string that is only a protected table's name are each found.
566
+ */
567
+ export function learningConstructs(source) {
568
+ const compiler = typescript();
569
+ const found = [];
570
+ const topOf = (node) => {
571
+ let at = node;
572
+ while (at.parent !== undefined && !compiler.isSourceFile(at.parent))
573
+ at = at.parent;
574
+ if (compiler.isFunctionDeclaration(at) || compiler.isClassDeclaration(at))
575
+ return at.name?.text ?? "";
576
+ if (compiler.isVariableStatement(at))
577
+ return at.declarationList.declarations.map(one => compiler.isIdentifier(one.name) ? one.name.text : "").join(",");
578
+ return "";
579
+ };
580
+ const fragmentRisk = (parts) => {
581
+ // Around each placeholder: the identifier characters just before and just after it.
582
+ for (let i = 0; i < parts.length - 1; i++) {
583
+ const before = /[A-Za-z0-9_]*$/.exec(parts[i])[0].toLowerCase(), after = /^[A-Za-z0-9_]*/.exec(parts[i + 1])[0].toLowerCase();
584
+ if (before === "" && after === "")
585
+ continue;
586
+ if (PROTECTED.some(table => (before !== "" && table.startsWith(before) && table !== before) || (after !== "" && table.endsWith(after) && table !== after)
587
+ || (before !== "" && after !== "" && table.startsWith(before) && table.endsWith(after))))
588
+ return true;
589
+ }
590
+ return false;
591
+ };
592
+ const consider = (node, parts) => {
593
+ const text = parts.join("${}");
594
+ const tables = TABLE_WRITES.filter(one => one.write.test(text)).map(one => one.table);
595
+ const unreadable = parts.length > 1 && WRITE_WORD.test(text) && fragmentRisk(parts);
596
+ const bare = parts.length === 1 && BARE_NAME.test(text);
597
+ if (tables.length > 0 || unreadable || bare)
598
+ found.push({ text: text.replace(/\s+/g, " ").trim(), tables, inside: topOf(node), unreadable, bare });
599
+ };
600
+ const isPlus = (node) => compiler.isBinaryExpression(node) && node.operatorToken.kind === compiler.SyntaxKind.PlusToken;
601
+ const flatten = (node, into) => {
602
+ let at = node;
603
+ while (compiler.isParenthesizedExpression(at))
604
+ at = at.expression;
605
+ if (isPlus(at)) {
606
+ flatten(at.left, into);
607
+ flatten(at.right, into);
608
+ }
609
+ else
610
+ into.push(at);
611
+ };
612
+ const visit = (node) => {
613
+ if (isPlus(node) && !(node.parent !== undefined && (isPlus(node.parent) || (compiler.isParenthesizedExpression(node.parent) && node.parent.parent !== undefined && isPlus(node.parent.parent))))) {
614
+ const operands = [];
615
+ flatten(node, operands);
616
+ if (operands.some(one => compiler.isStringLiteralLike(one) || compiler.isTemplateExpression(one))) {
617
+ const parts = [""];
618
+ for (const one of operands) {
619
+ if (compiler.isStringLiteralLike(one))
620
+ parts[parts.length - 1] += one.text;
621
+ else if (compiler.isTemplateExpression(one)) {
622
+ parts[parts.length - 1] += one.head.text;
623
+ for (const span of one.templateSpans)
624
+ parts.push(span.literal.text);
625
+ }
626
+ else
627
+ parts.push("");
628
+ }
629
+ if (parts.length > 1 || operands.length > 1)
630
+ consider(node, parts);
631
+ }
632
+ }
633
+ if (compiler.isStringLiteralLike(node))
634
+ consider(node, [node.text]);
635
+ else if (compiler.isTemplateExpression(node))
636
+ consider(node, [node.head.text, ...node.templateSpans.map(span => span.literal.text)]);
637
+ compiler.forEachChild(node, visit);
638
+ };
639
+ visit(source);
640
+ return found;
641
+ }
642
+ /** Whether this construct may stand in this file: its writer module for every table it writes, or the migration's own
643
+ * declarations in store.ts. An unreadable or bare construct stands only in a writer of some protected table. */
644
+ function learningAllowed(file, one) {
645
+ if ((LEARNING_MIGRATION[file] ?? []).some(name => one.inside.split(",").includes(name)))
646
+ return true;
647
+ if (one.tables.length > 0)
648
+ return one.tables.every(table => LEARNING_WRITERS[table].includes(file));
649
+ return Object.values(LEARNING_WRITERS).some(writers => writers.includes(file));
650
+ }
651
+ /**
652
+ * Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
653
+ * (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
654
+ * construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
655
+ * (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
656
+ * text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
657
+ */
658
+ export function learningWriteProblem(paths, candidate, base) {
659
+ const scanned = paths.filter(path => /\.(?:[cm]?[jt]sx?)$/.test(path) && !/\.test\.[cm]?[jt]sx?$/.test(path) && !path.startsWith("test/")
660
+ && !path.startsWith("node_modules/") && !path.includes("/node_modules/"));
661
+ if (scanned.length === 0)
662
+ return null;
663
+ if (candidate === null || base === null)
664
+ return "the changed files can't be read";
665
+ const key = (text) => createHash("sha256").update(text).digest("hex");
666
+ for (const path of scanned) {
667
+ const bytes = candidate(path);
668
+ if (bytes === null)
669
+ return `${path} can't be read`;
670
+ const now = parse(path, bytes);
671
+ if ("problem" in now)
672
+ return now.problem;
673
+ const before = base(path);
674
+ const was = before === null ? null : parse(path, before);
675
+ const kept = new Map();
676
+ if (was !== null && !("problem" in was))
677
+ for (const one of learningConstructs(was.source))
678
+ kept.set(key(one.text), (kept.get(key(one.text)) ?? 0) + 1);
679
+ for (const one of learningConstructs(now.source)) {
680
+ if (learningAllowed(path, one))
681
+ continue;
682
+ const left = kept.get(key(one.text)) ?? 0;
683
+ if (left > 0) {
684
+ kept.set(key(one.text), left - 1);
685
+ continue;
686
+ }
687
+ const what = one.unreadable ? "builds a learning-loops table's name around an expression in SQL" : one.bare ? "names a learning-loops table outside its writers" : "writes learning-loops rows outside their writers";
688
+ return `${path} ${what} (\`${one.text.slice(0, 80)}\`)`;
689
+ }
690
+ }
691
+ return null;
692
+ }
693
+ /**
694
+ * Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
695
+ * pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
696
+ * the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
697
+ */
698
+ export function learningHookProblem(manifest, changed) {
699
+ return evalHookProblem(manifest.candidate, manifest.base, LEARNING_HOOKS, LEARNING_PINNED) ?? learningWriteProblem(changed.paths, changed.candidate, changed.base);
700
+ }
package/dist/evals.js CHANGED
@@ -27,6 +27,7 @@ import { createHash, randomBytes } from "node:crypto";
27
27
  import { closeSync, constants, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, realpathSync, rmSync, writeSync } from "node:fs";
28
28
  import { dirname, join, relative, sep } from "node:path";
29
29
  import { homedir, hostname } from "node:os";
30
+ import { sameMachineHost } from "./machine-identity.js";
30
31
  import { fileURLToPath } from "node:url";
31
32
  import { createRequire } from "node:module";
32
33
  import { readVerifiedCommit, verifiedBlobs } from "./commit-extract.js";
@@ -840,7 +841,7 @@ export function recoverEvalCampaigns(store, now, options = {}) {
840
841
  const out = [];
841
842
  for (const campaign of store.handle.prepare("SELECT id, runner_host, runner_pid, runner_started FROM eval_campaign WHERE state = 'running' ORDER BY admitted_at").all()) {
842
843
  const id = String(campaign["id"]);
843
- if (String(campaign["runner_host"]) !== host) {
844
+ if (!sameMachineHost(String(campaign["runner_host"]), host)) {
844
845
  out.push({ id, recovered: false, why: `its runner is on ${String(campaign["runner_host"])}` });
845
846
  continue;
846
847
  }