looply-cli 0.0.0-stage → 0.2.0

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 (785) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -3
  3. package/dist/.src-hash +1 -0
  4. package/dist/acceptance-observe.d.ts +108 -0
  5. package/dist/acceptance-observe.d.ts.map +1 -0
  6. package/dist/acceptance-observe.js +152 -0
  7. package/dist/acceptance-observe.js.map +1 -0
  8. package/dist/agent-cmd.d.ts +131 -0
  9. package/dist/agent-cmd.d.ts.map +1 -0
  10. package/dist/agent-cmd.js +154 -0
  11. package/dist/agent-cmd.js.map +1 -0
  12. package/dist/agent-contained.d.ts +114 -0
  13. package/dist/agent-contained.d.ts.map +1 -0
  14. package/dist/agent-contained.js +292 -0
  15. package/dist/agent-contained.js.map +1 -0
  16. package/dist/agent-driver.d.ts +130 -0
  17. package/dist/agent-driver.d.ts.map +1 -0
  18. package/dist/agent-driver.js +170 -0
  19. package/dist/agent-driver.js.map +1 -0
  20. package/dist/agent-envelope.d.ts +94 -0
  21. package/dist/agent-envelope.d.ts.map +1 -0
  22. package/dist/agent-envelope.js +214 -0
  23. package/dist/agent-envelope.js.map +1 -0
  24. package/dist/agent-exec.d.ts +182 -0
  25. package/dist/agent-exec.d.ts.map +1 -0
  26. package/dist/agent-exec.js +125 -0
  27. package/dist/agent-exec.js.map +1 -0
  28. package/dist/agent-runtime.d.ts +23 -0
  29. package/dist/agent-runtime.d.ts.map +1 -0
  30. package/dist/agent-runtime.js +143 -0
  31. package/dist/agent-runtime.js.map +1 -0
  32. package/dist/approval-gate-read.d.ts +66 -0
  33. package/dist/approval-gate-read.d.ts.map +1 -0
  34. package/dist/approval-gate-read.js +138 -0
  35. package/dist/approval-gate-read.js.map +1 -0
  36. package/dist/approval-resolution.d.ts +87 -0
  37. package/dist/approval-resolution.d.ts.map +1 -0
  38. package/dist/approval-resolution.js +100 -0
  39. package/dist/approval-resolution.js.map +1 -0
  40. package/dist/approval-resumes.d.ts +102 -0
  41. package/dist/approval-resumes.d.ts.map +1 -0
  42. package/dist/approval-resumes.js +167 -0
  43. package/dist/approval-resumes.js.map +1 -0
  44. package/dist/ask-brain.d.ts +167 -0
  45. package/dist/ask-brain.d.ts.map +1 -0
  46. package/dist/ask-brain.js +355 -0
  47. package/dist/ask-brain.js.map +1 -0
  48. package/dist/autonomy-authority.d.ts +80 -0
  49. package/dist/autonomy-authority.d.ts.map +1 -0
  50. package/dist/autonomy-authority.js +82 -0
  51. package/dist/autonomy-authority.js.map +1 -0
  52. package/dist/autonomy-controller.d.ts +321 -0
  53. package/dist/autonomy-controller.d.ts.map +1 -0
  54. package/dist/autonomy-controller.js +517 -0
  55. package/dist/autonomy-controller.js.map +1 -0
  56. package/dist/autonomy-store.d.ts +327 -0
  57. package/dist/autonomy-store.d.ts.map +1 -0
  58. package/dist/autonomy-store.js +581 -0
  59. package/dist/autonomy-store.js.map +1 -0
  60. package/dist/autonomy-tick.d.ts +257 -0
  61. package/dist/autonomy-tick.d.ts.map +1 -0
  62. package/dist/autonomy-tick.js +616 -0
  63. package/dist/autonomy-tick.js.map +1 -0
  64. package/dist/bin.d.ts +3 -0
  65. package/dist/bin.d.ts.map +1 -0
  66. package/dist/bin.js +2719 -0
  67. package/dist/bin.js.map +1 -0
  68. package/dist/brain-advice.d.ts +21 -0
  69. package/dist/brain-advice.d.ts.map +1 -0
  70. package/dist/brain-advice.js +51 -0
  71. package/dist/brain-advice.js.map +1 -0
  72. package/dist/candidate-replay-runtime.d.ts +125 -0
  73. package/dist/candidate-replay-runtime.d.ts.map +1 -0
  74. package/dist/candidate-replay-runtime.js +188 -0
  75. package/dist/candidate-replay-runtime.js.map +1 -0
  76. package/dist/checkout-carry.d.ts +76 -0
  77. package/dist/checkout-carry.d.ts.map +1 -0
  78. package/dist/checkout-carry.js +125 -0
  79. package/dist/checkout-carry.js.map +1 -0
  80. package/dist/citation-readers.d.ts +26 -0
  81. package/dist/citation-readers.d.ts.map +1 -0
  82. package/dist/citation-readers.js +102 -0
  83. package/dist/citation-readers.js.map +1 -0
  84. package/dist/command-receipts.d.ts +79 -0
  85. package/dist/command-receipts.d.ts.map +1 -0
  86. package/dist/command-receipts.js +143 -0
  87. package/dist/command-receipts.js.map +1 -0
  88. package/dist/command-runner.d.ts +32 -0
  89. package/dist/command-runner.d.ts.map +1 -0
  90. package/dist/command-runner.js +108 -0
  91. package/dist/command-runner.js.map +1 -0
  92. package/dist/compatible-sets.d.ts +67 -0
  93. package/dist/compatible-sets.d.ts.map +1 -0
  94. package/dist/compatible-sets.js +81 -0
  95. package/dist/compatible-sets.js.map +1 -0
  96. package/dist/compile-check.d.ts +31 -0
  97. package/dist/compile-check.d.ts.map +1 -0
  98. package/dist/compile-check.js +34 -0
  99. package/dist/compile-check.js.map +1 -0
  100. package/dist/compile-store.d.ts +16 -0
  101. package/dist/compile-store.d.ts.map +1 -0
  102. package/dist/compile-store.js +21 -0
  103. package/dist/compile-store.js.map +1 -0
  104. package/dist/completion.d.ts +106 -0
  105. package/dist/completion.d.ts.map +1 -0
  106. package/dist/completion.js +131 -0
  107. package/dist/completion.js.map +1 -0
  108. package/dist/config.d.ts +549 -0
  109. package/dist/config.d.ts.map +1 -0
  110. package/dist/config.js +763 -0
  111. package/dist/config.js.map +1 -0
  112. package/dist/control-file.d.ts +142 -0
  113. package/dist/control-file.d.ts.map +1 -0
  114. package/dist/control-file.js +144 -0
  115. package/dist/control-file.js.map +1 -0
  116. package/dist/coordinator-state.d.ts +131 -0
  117. package/dist/coordinator-state.d.ts.map +1 -0
  118. package/dist/coordinator-state.js +380 -0
  119. package/dist/coordinator-state.js.map +1 -0
  120. package/dist/coordinator.d.ts +95 -0
  121. package/dist/coordinator.d.ts.map +1 -0
  122. package/dist/coordinator.js +97 -0
  123. package/dist/coordinator.js.map +1 -0
  124. package/dist/cursor-store.d.ts +53 -0
  125. package/dist/cursor-store.d.ts.map +1 -0
  126. package/dist/cursor-store.js +98 -0
  127. package/dist/cursor-store.js.map +1 -0
  128. package/dist/daemon-cmd.d.ts +239 -0
  129. package/dist/daemon-cmd.d.ts.map +1 -0
  130. package/dist/daemon-cmd.js +1751 -0
  131. package/dist/daemon-cmd.js.map +1 -0
  132. package/dist/daemon-lock.d.ts +140 -0
  133. package/dist/daemon-lock.d.ts.map +1 -0
  134. package/dist/daemon-lock.js +352 -0
  135. package/dist/daemon-lock.js.map +1 -0
  136. package/dist/daemon-recovery.d.ts +78 -0
  137. package/dist/daemon-recovery.d.ts.map +1 -0
  138. package/dist/daemon-recovery.js +161 -0
  139. package/dist/daemon-recovery.js.map +1 -0
  140. package/dist/daemon.d.ts +368 -0
  141. package/dist/daemon.d.ts.map +1 -0
  142. package/dist/daemon.js +601 -0
  143. package/dist/daemon.js.map +1 -0
  144. package/dist/declared-response-shape.d.ts +93 -0
  145. package/dist/declared-response-shape.d.ts.map +1 -0
  146. package/dist/declared-response-shape.js +200 -0
  147. package/dist/declared-response-shape.js.map +1 -0
  148. package/dist/declared-surfaces.d.ts +16 -0
  149. package/dist/declared-surfaces.d.ts.map +1 -0
  150. package/dist/declared-surfaces.js +19 -0
  151. package/dist/declared-surfaces.js.map +1 -0
  152. package/dist/depends-review.d.ts +113 -0
  153. package/dist/depends-review.d.ts.map +1 -0
  154. package/dist/depends-review.js +110 -0
  155. package/dist/depends-review.js.map +1 -0
  156. package/dist/doctor.d.ts +91 -0
  157. package/dist/doctor.d.ts.map +1 -0
  158. package/dist/doctor.js +71 -0
  159. package/dist/doctor.js.map +1 -0
  160. package/dist/edge-approval.d.ts +105 -0
  161. package/dist/edge-approval.d.ts.map +1 -0
  162. package/dist/edge-approval.js +100 -0
  163. package/dist/edge-approval.js.map +1 -0
  164. package/dist/evidence-cache.d.ts +45 -0
  165. package/dist/evidence-cache.d.ts.map +1 -0
  166. package/dist/evidence-cache.js +90 -0
  167. package/dist/evidence-cache.js.map +1 -0
  168. package/dist/evidence-cmd.d.ts +133 -0
  169. package/dist/evidence-cmd.d.ts.map +1 -0
  170. package/dist/evidence-cmd.js +347 -0
  171. package/dist/evidence-cmd.js.map +1 -0
  172. package/dist/evidence-facts.d.ts +40 -0
  173. package/dist/evidence-facts.d.ts.map +1 -0
  174. package/dist/evidence-facts.js +114 -0
  175. package/dist/evidence-facts.js.map +1 -0
  176. package/dist/exclusive-lock.d.ts +52 -0
  177. package/dist/exclusive-lock.d.ts.map +1 -0
  178. package/dist/exclusive-lock.js +150 -0
  179. package/dist/exclusive-lock.js.map +1 -0
  180. package/dist/exec-domains.d.ts +140 -0
  181. package/dist/exec-domains.d.ts.map +1 -0
  182. package/dist/exec-domains.js +287 -0
  183. package/dist/exec-domains.js.map +1 -0
  184. package/dist/exemption-store.d.ts +197 -0
  185. package/dist/exemption-store.d.ts.map +1 -0
  186. package/dist/exemption-store.js +266 -0
  187. package/dist/exemption-store.js.map +1 -0
  188. package/dist/explicit-start-gate.d.ts +96 -0
  189. package/dist/explicit-start-gate.d.ts.map +1 -0
  190. package/dist/explicit-start-gate.js +103 -0
  191. package/dist/explicit-start-gate.js.map +1 -0
  192. package/dist/fs-trouble.d.ts +70 -0
  193. package/dist/fs-trouble.d.ts.map +1 -0
  194. package/dist/fs-trouble.js +98 -0
  195. package/dist/fs-trouble.js.map +1 -0
  196. package/dist/gate.d.ts +278 -0
  197. package/dist/gate.d.ts.map +1 -0
  198. package/dist/gate.js +278 -0
  199. package/dist/gate.js.map +1 -0
  200. package/dist/git-driver.d.ts +519 -0
  201. package/dist/git-driver.d.ts.map +1 -0
  202. package/dist/git-driver.js +900 -0
  203. package/dist/git-driver.js.map +1 -0
  204. package/dist/git-push-executor.d.ts +41 -0
  205. package/dist/git-push-executor.d.ts.map +1 -0
  206. package/dist/git-push-executor.js +142 -0
  207. package/dist/git-push-executor.js.map +1 -0
  208. package/dist/glob.d.ts +26 -0
  209. package/dist/glob.d.ts.map +1 -0
  210. package/dist/glob.js +93 -0
  211. package/dist/glob.js.map +1 -0
  212. package/dist/goal-adaptive.d.ts +295 -0
  213. package/dist/goal-adaptive.d.ts.map +1 -0
  214. package/dist/goal-adaptive.js +393 -0
  215. package/dist/goal-adaptive.js.map +1 -0
  216. package/dist/goal-compile.d.ts +342 -0
  217. package/dist/goal-compile.d.ts.map +1 -0
  218. package/dist/goal-compile.js +444 -0
  219. package/dist/goal-compile.js.map +1 -0
  220. package/dist/goal-depends-executor.d.ts +55 -0
  221. package/dist/goal-depends-executor.d.ts.map +1 -0
  222. package/dist/goal-depends-executor.js +202 -0
  223. package/dist/goal-depends-executor.js.map +1 -0
  224. package/dist/goal-execution-evidence.d.ts +135 -0
  225. package/dist/goal-execution-evidence.d.ts.map +1 -0
  226. package/dist/goal-execution-evidence.js +159 -0
  227. package/dist/goal-execution-evidence.js.map +1 -0
  228. package/dist/goal-gap.d.ts +140 -0
  229. package/dist/goal-gap.d.ts.map +1 -0
  230. package/dist/goal-gap.js +197 -0
  231. package/dist/goal-gap.js.map +1 -0
  232. package/dist/goal-met-target.d.ts +98 -0
  233. package/dist/goal-met-target.d.ts.map +1 -0
  234. package/dist/goal-met-target.js +92 -0
  235. package/dist/goal-met-target.js.map +1 -0
  236. package/dist/goal-ownership.d.ts +416 -0
  237. package/dist/goal-ownership.d.ts.map +1 -0
  238. package/dist/goal-ownership.js +576 -0
  239. package/dist/goal-ownership.js.map +1 -0
  240. package/dist/goal-runs.d.ts +82 -0
  241. package/dist/goal-runs.d.ts.map +1 -0
  242. package/dist/goal-runs.js +120 -0
  243. package/dist/goal-runs.js.map +1 -0
  244. package/dist/goal-store.d.ts +146 -0
  245. package/dist/goal-store.d.ts.map +1 -0
  246. package/dist/goal-store.js +294 -0
  247. package/dist/goal-store.js.map +1 -0
  248. package/dist/goal-view.d.ts +156 -0
  249. package/dist/goal-view.d.ts.map +1 -0
  250. package/dist/goal-view.js +499 -0
  251. package/dist/goal-view.js.map +1 -0
  252. package/dist/governed-approvals.d.ts +152 -0
  253. package/dist/governed-approvals.d.ts.map +1 -0
  254. package/dist/governed-approvals.js +246 -0
  255. package/dist/governed-approvals.js.map +1 -0
  256. package/dist/governed-broker.d.ts +169 -0
  257. package/dist/governed-broker.d.ts.map +1 -0
  258. package/dist/governed-broker.js +196 -0
  259. package/dist/governed-broker.js.map +1 -0
  260. package/dist/governed-run.d.ts +220 -0
  261. package/dist/governed-run.d.ts.map +1 -0
  262. package/dist/governed-run.js +406 -0
  263. package/dist/governed-run.js.map +1 -0
  264. package/dist/grant-reconcile.d.ts +63 -0
  265. package/dist/grant-reconcile.d.ts.map +1 -0
  266. package/dist/grant-reconcile.js +121 -0
  267. package/dist/grant-reconcile.js.map +1 -0
  268. package/dist/index-for-head.d.ts +56 -0
  269. package/dist/index-for-head.d.ts.map +1 -0
  270. package/dist/index-for-head.js +103 -0
  271. package/dist/index-for-head.js.map +1 -0
  272. package/dist/index.d.ts +39 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +55 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/init-detect.d.ts +39 -0
  277. package/dist/init-detect.d.ts.map +1 -0
  278. package/dist/init-detect.js +188 -0
  279. package/dist/init-detect.js.map +1 -0
  280. package/dist/integration-deps.d.ts +62 -0
  281. package/dist/integration-deps.d.ts.map +1 -0
  282. package/dist/integration-deps.js +251 -0
  283. package/dist/integration-deps.js.map +1 -0
  284. package/dist/integration-runtime.d.ts +458 -0
  285. package/dist/integration-runtime.d.ts.map +1 -0
  286. package/dist/integration-runtime.js +681 -0
  287. package/dist/integration-runtime.js.map +1 -0
  288. package/dist/integration.d.ts +45 -0
  289. package/dist/integration.d.ts.map +1 -0
  290. package/dist/integration.js +87 -0
  291. package/dist/integration.js.map +1 -0
  292. package/dist/integrator.d.ts +84 -0
  293. package/dist/integrator.d.ts.map +1 -0
  294. package/dist/integrator.js +193 -0
  295. package/dist/integrator.js.map +1 -0
  296. package/dist/lane-claims.d.ts +174 -0
  297. package/dist/lane-claims.d.ts.map +1 -0
  298. package/dist/lane-claims.js +218 -0
  299. package/dist/lane-claims.js.map +1 -0
  300. package/dist/lane-coverage.d.ts +87 -0
  301. package/dist/lane-coverage.d.ts.map +1 -0
  302. package/dist/lane-coverage.js +129 -0
  303. package/dist/lane-coverage.js.map +1 -0
  304. package/dist/ledger-replay.d.ts +115 -0
  305. package/dist/ledger-replay.d.ts.map +1 -0
  306. package/dist/ledger-replay.js +155 -0
  307. package/dist/ledger-replay.js.map +1 -0
  308. package/dist/ledger-store.d.ts +68 -0
  309. package/dist/ledger-store.d.ts.map +1 -0
  310. package/dist/ledger-store.js +111 -0
  311. package/dist/ledger-store.js.map +1 -0
  312. package/dist/loop-safety-observer.d.ts +132 -0
  313. package/dist/loop-safety-observer.d.ts.map +1 -0
  314. package/dist/loop-safety-observer.js +76 -0
  315. package/dist/loop-safety-observer.js.map +1 -0
  316. package/dist/loop-safety-store.d.ts +339 -0
  317. package/dist/loop-safety-store.d.ts.map +1 -0
  318. package/dist/loop-safety-store.js +800 -0
  319. package/dist/loop-safety-store.js.map +1 -0
  320. package/dist/machine-store.d.ts +54 -0
  321. package/dist/machine-store.d.ts.map +1 -0
  322. package/dist/machine-store.js +149 -0
  323. package/dist/machine-store.js.map +1 -0
  324. package/dist/mcp-main.d.ts +2 -0
  325. package/dist/mcp-main.d.ts.map +1 -0
  326. package/dist/mcp-main.js +116 -0
  327. package/dist/mcp-main.js.map +1 -0
  328. package/dist/mcp-server.d.ts +72 -0
  329. package/dist/mcp-server.d.ts.map +1 -0
  330. package/dist/mcp-server.js +259 -0
  331. package/dist/mcp-server.js.map +1 -0
  332. package/dist/model-routing.d.ts +75 -0
  333. package/dist/model-routing.d.ts.map +1 -0
  334. package/dist/model-routing.js +108 -0
  335. package/dist/model-routing.js.map +1 -0
  336. package/dist/model-transport.d.ts +188 -0
  337. package/dist/model-transport.d.ts.map +1 -0
  338. package/dist/model-transport.js +625 -0
  339. package/dist/model-transport.js.map +1 -0
  340. package/dist/notify-cadence.d.ts +55 -0
  341. package/dist/notify-cadence.d.ts.map +1 -0
  342. package/dist/notify-cadence.js +68 -0
  343. package/dist/notify-cadence.js.map +1 -0
  344. package/dist/notify-channel.d.ts +69 -0
  345. package/dist/notify-channel.d.ts.map +1 -0
  346. package/dist/notify-channel.js +151 -0
  347. package/dist/notify-channel.js.map +1 -0
  348. package/dist/notify.d.ts +71 -0
  349. package/dist/notify.d.ts.map +1 -0
  350. package/dist/notify.js +125 -0
  351. package/dist/notify.js.map +1 -0
  352. package/dist/observation-inventory.d.ts +65 -0
  353. package/dist/observation-inventory.d.ts.map +1 -0
  354. package/dist/observation-inventory.js +139 -0
  355. package/dist/observation-inventory.js.map +1 -0
  356. package/dist/owned-children.d.ts +45 -0
  357. package/dist/owned-children.d.ts.map +1 -0
  358. package/dist/owned-children.js +64 -0
  359. package/dist/owned-children.js.map +1 -0
  360. package/dist/packet-for-goal.d.ts +19 -0
  361. package/dist/packet-for-goal.d.ts.map +1 -0
  362. package/dist/packet-for-goal.js +32 -0
  363. package/dist/packet-for-goal.js.map +1 -0
  364. package/dist/pair-cmd.d.ts +85 -0
  365. package/dist/pair-cmd.d.ts.map +1 -0
  366. package/dist/pair-cmd.js +205 -0
  367. package/dist/pair-cmd.js.map +1 -0
  368. package/dist/partial-asker.d.ts +96 -0
  369. package/dist/partial-asker.d.ts.map +1 -0
  370. package/dist/partial-asker.js +117 -0
  371. package/dist/partial-asker.js.map +1 -0
  372. package/dist/pdf-text-lines.d.ts +17 -0
  373. package/dist/pdf-text-lines.d.ts.map +1 -0
  374. package/dist/pdf-text-lines.js +70 -0
  375. package/dist/pdf-text-lines.js.map +1 -0
  376. package/dist/pdf-text-worker.d.ts +2 -0
  377. package/dist/pdf-text-worker.d.ts.map +1 -0
  378. package/dist/pdf-text-worker.js +81 -0
  379. package/dist/pdf-text-worker.js.map +1 -0
  380. package/dist/pdf-text.d.ts +56 -0
  381. package/dist/pdf-text.d.ts.map +1 -0
  382. package/dist/pdf-text.js +64 -0
  383. package/dist/pdf-text.js.map +1 -0
  384. package/dist/pending-action.d.ts +154 -0
  385. package/dist/pending-action.d.ts.map +1 -0
  386. package/dist/pending-action.js +148 -0
  387. package/dist/pending-action.js.map +1 -0
  388. package/dist/preemptor.d.ts +40 -0
  389. package/dist/preemptor.d.ts.map +1 -0
  390. package/dist/preemptor.js +49 -0
  391. package/dist/preemptor.js.map +1 -0
  392. package/dist/programme-mode.d.ts +29 -0
  393. package/dist/programme-mode.d.ts.map +1 -0
  394. package/dist/programme-mode.js +50 -0
  395. package/dist/programme-mode.js.map +1 -0
  396. package/dist/progress-view.d.ts +14 -0
  397. package/dist/progress-view.d.ts.map +1 -0
  398. package/dist/progress-view.js +114 -0
  399. package/dist/progress-view.js.map +1 -0
  400. package/dist/project-graph-builder.d.ts +81 -0
  401. package/dist/project-graph-builder.d.ts.map +1 -0
  402. package/dist/project-graph-builder.js +246 -0
  403. package/dist/project-graph-builder.js.map +1 -0
  404. package/dist/project-graph-runtime.d.ts +86 -0
  405. package/dist/project-graph-runtime.d.ts.map +1 -0
  406. package/dist/project-graph-runtime.js +112 -0
  407. package/dist/project-graph-runtime.js.map +1 -0
  408. package/dist/project-index-builder.d.ts +96 -0
  409. package/dist/project-index-builder.d.ts.map +1 -0
  410. package/dist/project-index-builder.js +298 -0
  411. package/dist/project-index-builder.js.map +1 -0
  412. package/dist/project-index-facts.d.ts +51 -0
  413. package/dist/project-index-facts.d.ts.map +1 -0
  414. package/dist/project-index-facts.js +117 -0
  415. package/dist/project-index-facts.js.map +1 -0
  416. package/dist/project-index-store.d.ts +97 -0
  417. package/dist/project-index-store.d.ts.map +1 -0
  418. package/dist/project-index-store.js +140 -0
  419. package/dist/project-index-store.js.map +1 -0
  420. package/dist/project-memory-store.d.ts +127 -0
  421. package/dist/project-memory-store.d.ts.map +1 -0
  422. package/dist/project-memory-store.js +302 -0
  423. package/dist/project-memory-store.js.map +1 -0
  424. package/dist/proposal-store.d.ts +14 -0
  425. package/dist/proposal-store.d.ts.map +1 -0
  426. package/dist/proposal-store.js +19 -0
  427. package/dist/proposal-store.js.map +1 -0
  428. package/dist/prover.d.ts +80 -0
  429. package/dist/prover.d.ts.map +1 -0
  430. package/dist/prover.js +137 -0
  431. package/dist/prover.js.map +1 -0
  432. package/dist/provider-identity-store.d.ts +34 -0
  433. package/dist/provider-identity-store.d.ts.map +1 -0
  434. package/dist/provider-identity-store.js +72 -0
  435. package/dist/provider-identity-store.js.map +1 -0
  436. package/dist/provider-identity.d.ts +104 -0
  437. package/dist/provider-identity.d.ts.map +1 -0
  438. package/dist/provider-identity.js +167 -0
  439. package/dist/provider-identity.js.map +1 -0
  440. package/dist/provider-limit.d.ts +79 -0
  441. package/dist/provider-limit.d.ts.map +1 -0
  442. package/dist/provider-limit.js +148 -0
  443. package/dist/provider-limit.js.map +1 -0
  444. package/dist/provider-preflight.d.ts +171 -0
  445. package/dist/provider-preflight.d.ts.map +1 -0
  446. package/dist/provider-preflight.js +395 -0
  447. package/dist/provider-preflight.js.map +1 -0
  448. package/dist/provider-state.d.ts +120 -0
  449. package/dist/provider-state.d.ts.map +1 -0
  450. package/dist/provider-state.js +182 -0
  451. package/dist/provider-state.js.map +1 -0
  452. package/dist/providers.d.ts +132 -0
  453. package/dist/providers.d.ts.map +1 -0
  454. package/dist/providers.js +161 -0
  455. package/dist/providers.js.map +1 -0
  456. package/dist/publication-recorder.d.ts +117 -0
  457. package/dist/publication-recorder.d.ts.map +1 -0
  458. package/dist/publication-recorder.js +124 -0
  459. package/dist/publication-recorder.js.map +1 -0
  460. package/dist/pursuit-delivery.d.ts +24 -0
  461. package/dist/pursuit-delivery.d.ts.map +1 -0
  462. package/dist/pursuit-delivery.js +38 -0
  463. package/dist/pursuit-delivery.js.map +1 -0
  464. package/dist/queue-store.d.ts +132 -0
  465. package/dist/queue-store.d.ts.map +1 -0
  466. package/dist/queue-store.js +185 -0
  467. package/dist/queue-store.js.map +1 -0
  468. package/dist/queue-summary.d.ts +14 -0
  469. package/dist/queue-summary.d.ts.map +1 -0
  470. package/dist/queue-summary.js +82 -0
  471. package/dist/queue-summary.js.map +1 -0
  472. package/dist/queue-view.d.ts +20 -0
  473. package/dist/queue-view.d.ts.map +1 -0
  474. package/dist/queue-view.js +163 -0
  475. package/dist/queue-view.js.map +1 -0
  476. package/dist/queue-write-lease.d.ts +106 -0
  477. package/dist/queue-write-lease.d.ts.map +1 -0
  478. package/dist/queue-write-lease.js +282 -0
  479. package/dist/queue-write-lease.js.map +1 -0
  480. package/dist/rails-home.d.ts +162 -0
  481. package/dist/rails-home.d.ts.map +1 -0
  482. package/dist/rails-home.js +452 -0
  483. package/dist/rails-home.js.map +1 -0
  484. package/dist/rails-migrations.d.ts +75 -0
  485. package/dist/rails-migrations.d.ts.map +1 -0
  486. package/dist/rails-migrations.js +199 -0
  487. package/dist/rails-migrations.js.map +1 -0
  488. package/dist/rails-stores.d.ts +49 -0
  489. package/dist/rails-stores.d.ts.map +1 -0
  490. package/dist/rails-stores.js +100 -0
  491. package/dist/rails-stores.js.map +1 -0
  492. package/dist/reaper.d.ts +69 -0
  493. package/dist/reaper.d.ts.map +1 -0
  494. package/dist/reaper.js +152 -0
  495. package/dist/reaper.js.map +1 -0
  496. package/dist/record-store.d.ts +36 -0
  497. package/dist/record-store.d.ts.map +1 -0
  498. package/dist/record-store.js +78 -0
  499. package/dist/record-store.js.map +1 -0
  500. package/dist/recovery-io.d.ts +50 -0
  501. package/dist/recovery-io.d.ts.map +1 -0
  502. package/dist/recovery-io.js +122 -0
  503. package/dist/recovery-io.js.map +1 -0
  504. package/dist/recovery.d.ts +146 -0
  505. package/dist/recovery.d.ts.map +1 -0
  506. package/dist/recovery.js +110 -0
  507. package/dist/recovery.js.map +1 -0
  508. package/dist/redraw.d.ts +34 -0
  509. package/dist/redraw.d.ts.map +1 -0
  510. package/dist/redraw.js +62 -0
  511. package/dist/redraw.js.map +1 -0
  512. package/dist/rejected-evidence.d.ts +255 -0
  513. package/dist/rejected-evidence.d.ts.map +1 -0
  514. package/dist/rejected-evidence.js +324 -0
  515. package/dist/rejected-evidence.js.map +1 -0
  516. package/dist/report-cmd.d.ts +32 -0
  517. package/dist/report-cmd.d.ts.map +1 -0
  518. package/dist/report-cmd.js +201 -0
  519. package/dist/report-cmd.js.map +1 -0
  520. package/dist/repos-cmd.d.ts +80 -0
  521. package/dist/repos-cmd.d.ts.map +1 -0
  522. package/dist/repos-cmd.js +240 -0
  523. package/dist/repos-cmd.js.map +1 -0
  524. package/dist/request-body-shape.d.ts +93 -0
  525. package/dist/request-body-shape.d.ts.map +1 -0
  526. package/dist/request-body-shape.js +407 -0
  527. package/dist/request-body-shape.js.map +1 -0
  528. package/dist/requirement-claims.d.ts +89 -0
  529. package/dist/requirement-claims.d.ts.map +1 -0
  530. package/dist/requirement-claims.js +92 -0
  531. package/dist/requirement-claims.js.map +1 -0
  532. package/dist/retention-apply-runtime.d.ts +105 -0
  533. package/dist/retention-apply-runtime.d.ts.map +1 -0
  534. package/dist/retention-apply-runtime.js +269 -0
  535. package/dist/retention-apply-runtime.js.map +1 -0
  536. package/dist/reverify.d.ts +41 -0
  537. package/dist/reverify.d.ts.map +1 -0
  538. package/dist/reverify.js +69 -0
  539. package/dist/reverify.js.map +1 -0
  540. package/dist/round-records.d.ts +65 -0
  541. package/dist/round-records.d.ts.map +1 -0
  542. package/dist/round-records.js +61 -0
  543. package/dist/round-records.js.map +1 -0
  544. package/dist/round-runner.d.ts +924 -0
  545. package/dist/round-runner.d.ts.map +1 -0
  546. package/dist/round-runner.js +1693 -0
  547. package/dist/round-runner.js.map +1 -0
  548. package/dist/run-approval.d.ts +180 -0
  549. package/dist/run-approval.d.ts.map +1 -0
  550. package/dist/run-approval.js +199 -0
  551. package/dist/run-approval.js.map +1 -0
  552. package/dist/run-cmd.d.ts +359 -0
  553. package/dist/run-cmd.d.ts.map +1 -0
  554. package/dist/run-cmd.js +4418 -0
  555. package/dist/run-cmd.js.map +1 -0
  556. package/dist/run-delivery.d.ts +71 -0
  557. package/dist/run-delivery.d.ts.map +1 -0
  558. package/dist/run-delivery.js +104 -0
  559. package/dist/run-delivery.js.map +1 -0
  560. package/dist/run-ended.d.ts +75 -0
  561. package/dist/run-ended.d.ts.map +1 -0
  562. package/dist/run-ended.js +27 -0
  563. package/dist/run-ended.js.map +1 -0
  564. package/dist/run-ending-locator.d.ts +125 -0
  565. package/dist/run-ending-locator.d.ts.map +1 -0
  566. package/dist/run-ending-locator.js +133 -0
  567. package/dist/run-ending-locator.js.map +1 -0
  568. package/dist/run-launcher.d.ts +459 -0
  569. package/dist/run-launcher.d.ts.map +1 -0
  570. package/dist/run-launcher.js +652 -0
  571. package/dist/run-launcher.js.map +1 -0
  572. package/dist/run-lease.d.ts +208 -0
  573. package/dist/run-lease.d.ts.map +1 -0
  574. package/dist/run-lease.js +301 -0
  575. package/dist/run-lease.js.map +1 -0
  576. package/dist/run-refusal-marks.d.ts +189 -0
  577. package/dist/run-refusal-marks.d.ts.map +1 -0
  578. package/dist/run-refusal-marks.js +249 -0
  579. package/dist/run-refusal-marks.js.map +1 -0
  580. package/dist/run-start-marks.d.ts +153 -0
  581. package/dist/run-start-marks.d.ts.map +1 -0
  582. package/dist/run-start-marks.js +269 -0
  583. package/dist/run-start-marks.js.map +1 -0
  584. package/dist/scan-cmd.d.ts +14 -0
  585. package/dist/scan-cmd.d.ts.map +1 -0
  586. package/dist/scan-cmd.js +270 -0
  587. package/dist/scan-cmd.js.map +1 -0
  588. package/dist/scan-events.d.ts +50 -0
  589. package/dist/scan-events.d.ts.map +1 -0
  590. package/dist/scan-events.js +91 -0
  591. package/dist/scan-events.js.map +1 -0
  592. package/dist/scan.d.ts +191 -0
  593. package/dist/scan.d.ts.map +1 -0
  594. package/dist/scan.js +417 -0
  595. package/dist/scan.js.map +1 -0
  596. package/dist/scope-rule.d.ts +148 -0
  597. package/dist/scope-rule.d.ts.map +1 -0
  598. package/dist/scope-rule.js +121 -0
  599. package/dist/scope-rule.js.map +1 -0
  600. package/dist/self-repo-id.d.ts +18 -0
  601. package/dist/self-repo-id.d.ts.map +1 -0
  602. package/dist/self-repo-id.js +70 -0
  603. package/dist/self-repo-id.js.map +1 -0
  604. package/dist/server-client.d.ts +766 -0
  605. package/dist/server-client.d.ts.map +1 -0
  606. package/dist/server-client.js +475 -0
  607. package/dist/server-client.js.map +1 -0
  608. package/dist/session-adapter-runtime.d.ts +195 -0
  609. package/dist/session-adapter-runtime.d.ts.map +1 -0
  610. package/dist/session-adapter-runtime.js +373 -0
  611. package/dist/session-adapter-runtime.js.map +1 -0
  612. package/dist/session-checkpoint-runtime.d.ts +62 -0
  613. package/dist/session-checkpoint-runtime.d.ts.map +1 -0
  614. package/dist/session-checkpoint-runtime.js +268 -0
  615. package/dist/session-checkpoint-runtime.js.map +1 -0
  616. package/dist/session-control-transport.d.ts +106 -0
  617. package/dist/session-control-transport.d.ts.map +1 -0
  618. package/dist/session-control-transport.js +232 -0
  619. package/dist/session-control-transport.js.map +1 -0
  620. package/dist/session-observation-store.d.ts +98 -0
  621. package/dist/session-observation-store.d.ts.map +1 -0
  622. package/dist/session-observation-store.js +155 -0
  623. package/dist/session-observation-store.js.map +1 -0
  624. package/dist/session-observe-runtime.d.ts +123 -0
  625. package/dist/session-observe-runtime.d.ts.map +1 -0
  626. package/dist/session-observe-runtime.js +317 -0
  627. package/dist/session-observe-runtime.js.map +1 -0
  628. package/dist/session-store.d.ts +98 -0
  629. package/dist/session-store.d.ts.map +1 -0
  630. package/dist/session-store.js +116 -0
  631. package/dist/session-store.js.map +1 -0
  632. package/dist/sessions-cmd.d.ts +202 -0
  633. package/dist/sessions-cmd.d.ts.map +1 -0
  634. package/dist/sessions-cmd.js +609 -0
  635. package/dist/sessions-cmd.js.map +1 -0
  636. package/dist/settings-overlay.d.ts +53 -0
  637. package/dist/settings-overlay.d.ts.map +1 -0
  638. package/dist/settings-overlay.js +175 -0
  639. package/dist/settings-overlay.js.map +1 -0
  640. package/dist/setup-prompt.d.ts +34 -0
  641. package/dist/setup-prompt.d.ts.map +1 -0
  642. package/dist/setup-prompt.js +92 -0
  643. package/dist/setup-prompt.js.map +1 -0
  644. package/dist/setup.d.ts +103 -0
  645. package/dist/setup.d.ts.map +1 -0
  646. package/dist/setup.js +283 -0
  647. package/dist/setup.js.map +1 -0
  648. package/dist/shared-grants.d.ts +164 -0
  649. package/dist/shared-grants.d.ts.map +1 -0
  650. package/dist/shared-grants.js +242 -0
  651. package/dist/shared-grants.js.map +1 -0
  652. package/dist/signals.d.ts +55 -0
  653. package/dist/signals.d.ts.map +1 -0
  654. package/dist/signals.js +85 -0
  655. package/dist/signals.js.map +1 -0
  656. package/dist/source-cmd.d.ts +201 -0
  657. package/dist/source-cmd.d.ts.map +1 -0
  658. package/dist/source-cmd.js +684 -0
  659. package/dist/source-cmd.js.map +1 -0
  660. package/dist/source-verdict.d.ts +99 -0
  661. package/dist/source-verdict.d.ts.map +1 -0
  662. package/dist/source-verdict.js +182 -0
  663. package/dist/source-verdict.js.map +1 -0
  664. package/dist/spend-ledger.d.ts +350 -0
  665. package/dist/spend-ledger.d.ts.map +1 -0
  666. package/dist/spend-ledger.js +546 -0
  667. package/dist/spend-ledger.js.map +1 -0
  668. package/dist/stale-run-sweep.d.ts +79 -0
  669. package/dist/stale-run-sweep.d.ts.map +1 -0
  670. package/dist/stale-run-sweep.js +116 -0
  671. package/dist/stale-run-sweep.js.map +1 -0
  672. package/dist/start-attempts.d.ts +385 -0
  673. package/dist/start-attempts.d.ts.map +1 -0
  674. package/dist/start-attempts.js +613 -0
  675. package/dist/start-attempts.js.map +1 -0
  676. package/dist/status-cmd.d.ts +28 -0
  677. package/dist/status-cmd.d.ts.map +1 -0
  678. package/dist/status-cmd.js +116 -0
  679. package/dist/status-cmd.js.map +1 -0
  680. package/dist/stop-follow-up.d.ts +107 -0
  681. package/dist/stop-follow-up.d.ts.map +1 -0
  682. package/dist/stop-follow-up.js +172 -0
  683. package/dist/stop-follow-up.js.map +1 -0
  684. package/dist/surface-lease.d.ts +118 -0
  685. package/dist/surface-lease.d.ts.map +1 -0
  686. package/dist/surface-lease.js +485 -0
  687. package/dist/surface-lease.js.map +1 -0
  688. package/dist/sync.d.ts +54 -0
  689. package/dist/sync.d.ts.map +1 -0
  690. package/dist/sync.js +93 -0
  691. package/dist/sync.js.map +1 -0
  692. package/dist/syncing-ledger.d.ts +45 -0
  693. package/dist/syncing-ledger.d.ts.map +1 -0
  694. package/dist/syncing-ledger.js +53 -0
  695. package/dist/syncing-ledger.js.map +1 -0
  696. package/dist/temp-fixtures.d.ts +68 -0
  697. package/dist/temp-fixtures.d.ts.map +1 -0
  698. package/dist/temp-fixtures.js +38 -0
  699. package/dist/temp-fixtures.js.map +1 -0
  700. package/dist/token-window.d.ts +84 -0
  701. package/dist/token-window.d.ts.map +1 -0
  702. package/dist/token-window.js +143 -0
  703. package/dist/token-window.js.map +1 -0
  704. package/dist/tool-bridge.d.ts +177 -0
  705. package/dist/tool-bridge.d.ts.map +1 -0
  706. package/dist/tool-bridge.js +329 -0
  707. package/dist/tool-bridge.js.map +1 -0
  708. package/dist/tool-call-record.d.ts +197 -0
  709. package/dist/tool-call-record.d.ts.map +1 -0
  710. package/dist/tool-call-record.js +276 -0
  711. package/dist/tool-call-record.js.map +1 -0
  712. package/dist/tool-protocol.d.ts +135 -0
  713. package/dist/tool-protocol.d.ts.map +1 -0
  714. package/dist/tool-protocol.js +104 -0
  715. package/dist/tool-protocol.js.map +1 -0
  716. package/dist/tool-worker.d.ts +3 -0
  717. package/dist/tool-worker.d.ts.map +1 -0
  718. package/dist/tool-worker.js +478 -0
  719. package/dist/tool-worker.js.map +1 -0
  720. package/dist/toolchain-mirror.d.ts +86 -0
  721. package/dist/toolchain-mirror.d.ts.map +1 -0
  722. package/dist/toolchain-mirror.js +307 -0
  723. package/dist/toolchain-mirror.js.map +1 -0
  724. package/dist/trusted-git.d.ts +243 -0
  725. package/dist/trusted-git.d.ts.map +1 -0
  726. package/dist/trusted-git.js +920 -0
  727. package/dist/trusted-git.js.map +1 -0
  728. package/dist/ui.d.ts +76 -0
  729. package/dist/ui.d.ts.map +1 -0
  730. package/dist/ui.js +118 -0
  731. package/dist/ui.js.map +1 -0
  732. package/dist/untrusted-exec.d.ts +275 -0
  733. package/dist/untrusted-exec.d.ts.map +1 -0
  734. package/dist/untrusted-exec.js +479 -0
  735. package/dist/untrusted-exec.js.map +1 -0
  736. package/dist/untrusted-gate.d.ts +73 -0
  737. package/dist/untrusted-gate.d.ts.map +1 -0
  738. package/dist/untrusted-gate.js +226 -0
  739. package/dist/untrusted-gate.js.map +1 -0
  740. package/dist/uploader.d.ts +153 -0
  741. package/dist/uploader.d.ts.map +1 -0
  742. package/dist/uploader.js +204 -0
  743. package/dist/uploader.js.map +1 -0
  744. package/dist/verifier.d.ts +66 -0
  745. package/dist/verifier.d.ts.map +1 -0
  746. package/dist/verifier.js +170 -0
  747. package/dist/verifier.js.map +1 -0
  748. package/dist/verify-apply.d.ts +26 -0
  749. package/dist/verify-apply.d.ts.map +1 -0
  750. package/dist/verify-apply.js +36 -0
  751. package/dist/verify-apply.js.map +1 -0
  752. package/dist/verify-cmd.d.ts +44 -0
  753. package/dist/verify-cmd.d.ts.map +1 -0
  754. package/dist/verify-cmd.js +125 -0
  755. package/dist/verify-cmd.js.map +1 -0
  756. package/dist/verify-pass.d.ts +73 -0
  757. package/dist/verify-pass.d.ts.map +1 -0
  758. package/dist/verify-pass.js +103 -0
  759. package/dist/verify-pass.js.map +1 -0
  760. package/dist/welcome.d.ts +43 -0
  761. package/dist/welcome.d.ts.map +1 -0
  762. package/dist/welcome.js +116 -0
  763. package/dist/welcome.js.map +1 -0
  764. package/dist/workspace-provisioner.d.ts +65 -0
  765. package/dist/workspace-provisioner.d.ts.map +1 -0
  766. package/dist/workspace-provisioner.js +66 -0
  767. package/dist/workspace-provisioner.js.map +1 -0
  768. package/dist/workspace-quarantine.d.ts +64 -0
  769. package/dist/workspace-quarantine.d.ts.map +1 -0
  770. package/dist/workspace-quarantine.js +156 -0
  771. package/dist/workspace-quarantine.js.map +1 -0
  772. package/dist/workspace-residue-sweep.d.ts +113 -0
  773. package/dist/workspace-residue-sweep.d.ts.map +1 -0
  774. package/dist/workspace-residue-sweep.js +158 -0
  775. package/dist/workspace-residue-sweep.js.map +1 -0
  776. package/dist/workspace.d.ts +639 -0
  777. package/dist/workspace.d.ts.map +1 -0
  778. package/dist/workspace.js +2026 -0
  779. package/dist/workspace.js.map +1 -0
  780. package/package.json +54 -6
  781. package/win/CredProbe.cs +64 -0
  782. package/win/FakeClaude.cs +56 -0
  783. package/win/HandleProbe.cs +116 -0
  784. package/win/TokenProbe.cs +53 -0
  785. package/win/appcontainer.ps1 +500 -0
@@ -0,0 +1,1693 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RoundRunner = exports.DEFAULT_PAUSE_POLL_MS = exports.RunnerError = exports.PARTIAL_DISCARDED_CODE = exports.RUN_HALTED_CODE = exports.GATE_MACHINERY = exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS = void 0;
4
+ exports.partialDiscardedText = partialDiscardedText;
5
+ const core_1 = require("@looply/core");
6
+ const gate_1 = require("./gate");
7
+ const glob_1 = require("./glob");
8
+ const scope_rule_1 = require("./scope-rule");
9
+ const rejected_evidence_1 = require("./rejected-evidence");
10
+ const verify_apply_1 = require("./verify-apply");
11
+ const reverify_1 = require("./reverify");
12
+ /**
13
+ * How many rounds may fail to run their gate before the run gives up.
14
+ *
15
+ * A blocked round costs the item nothing, which is the point — and which is
16
+ * also why the loop needs its own floor: without one, a machine with no
17
+ * containment would pick, implement, fail to gate and return the item, for ever
18
+ * and at full model cost.
19
+ */
20
+ exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS = 3;
21
+ /**
22
+ * WHAT THE GATE'S COMMANDS RESOLVE THROUGH, AND WHAT ITS STEPS FIND FOR THEMSELVES.
23
+ *
24
+ * `judgedBy` refuses to guess either, and this is where the guessing is done
25
+ * instead — in one named place that can be read and argued with, rather than
26
+ * inside a pure fold where it would look like a fact.
27
+ *
28
+ * `defining` is short on purpose. A config that MATTERS to a step is almost
29
+ * always named by that step's own command — `npx tsc --noEmit -p
30
+ * packages/cli/tsconfig.json` names its tsconfig, and `judgedBy` establishes
31
+ * that exactly, from two texts. What is left is the handful a command reaches
32
+ * through without ever writing down: `npm run x` and `npx x` both resolve
33
+ * through `package.json`, and a runner reads its own config by convention.
34
+ * Every basename added here widens a claim, so the bar is that the file
35
+ * decides what a gate step DOES.
36
+ *
37
+ * ⚠️ `discovered` IS THE WEAKER TIER AND `judgedBy` KEEPS IT APART FROM THE
38
+ * REST. `npx --no-install jest --ci` names no spec file; jest globs for them,
39
+ * and this repository declares the shape in `jest.config.js` rather than in
40
+ * any gate step. So a changed spec file is a claim from the file NAME, not
41
+ * from a step's declaration, and the sentence a person reads says so. That is
42
+ * "make the weaker claim and say so" — never "the suite is unchanged", which
43
+ * is the one thing no path pattern can establish.
44
+ */
45
+ exports.GATE_MACHINERY = {
46
+ defining: ['package.json', 'jest.config.js', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs'],
47
+ discovered: ['.spec.ts', '.spec.tsx', '.test.ts', '.test.tsx', '.spec.js', '.test.js'],
48
+ };
49
+ /**
50
+ * THE ALERT A RUN WRITES WHEN IT HALTS ITSELF (M6-12), immediately before
51
+ * `run.ended`. Every {@link RunHalt} carries a `because`, and until M6-12
52
+ * that sentence reached nothing: `run.ended` carries `stopReason` and totals,
53
+ * the crash path wrote `run-aborted`, the halt path wrote nothing, and the run
54
+ * child has no log. A person's Start placed under the umbrella in a repo
55
+ * without `looply/integration` ended `aborted` after one round and nobody
56
+ * could read why (found live in M6-11). The reason is one sentence, never
57
+ * parsed; `warn` when the halt is an abort, `info` for any other halt.
58
+ */
59
+ exports.RUN_HALTED_CODE = 'run-halted';
60
+ /**
61
+ * THE ALERT A RUN WRITES WHEN A CHECKER WAS NOT SURE AND NOBODY COULD BE ASKED (M6-20).
62
+ *
63
+ * `proof.on_partial` defaults to `ask`, and nothing in this build can ask: no
64
+ * CLI code produces a `question.asked`, so `looply run`'s asker answers
65
+ * `discard` and says so to a terminal nobody is watching (the audit's F-03).
66
+ * The work a checker was unsure about was therefore thrown away in silence —
67
+ * the item's failure line said "proof partial: …", which reads as the checker
68
+ * having refused, and nothing anywhere said that the configured answer was to
69
+ * ASK and that asking was impossible.
70
+ *
71
+ * ⚠️ ONLY WHEN THE POLICY IS `ask`. A project that configured `discard` chose
72
+ * this; saying it back to them on every partial verdict would be noise. And the
73
+ * run still discards — making it ask is a change to the round protocol and the
74
+ * owner's decision, not this alert's business.
75
+ */
76
+ exports.PARTIAL_DISCARDED_CODE = 'proof-partial-discarded';
77
+ /** What a partial proof discarded for want of anyone to ask says, in one sentence. */
78
+ function partialDiscardedText(itemTitle, reason) {
79
+ return (`the checker was not sure about "${itemTitle}" — ${reason} — and nothing on this machine can be asked, ` +
80
+ 'so the work was discarded (proof.on_partial: ask)');
81
+ }
82
+ class RunnerError extends Error {
83
+ }
84
+ exports.RunnerError = RunnerError;
85
+ /** How often a paused run re-reads control.json. Overridable via `pausePollMs`. */
86
+ exports.DEFAULT_PAUSE_POLL_MS = 5_000;
87
+ class RoundRunner {
88
+ cfg;
89
+ deps;
90
+ id;
91
+ /**
92
+ * `proverRejected: 0` is set HERE rather than left absent, and the difference
93
+ * matters: this runner always counts, so a run in which the prover rejected
94
+ * nothing reports a PROVEN zero. Absent stays reserved for a producer that
95
+ * did not count at all — see {@link RunTotals.proverRejected}.
96
+ */
97
+ totals = { rounds: 0, committed: 0, discarded: 0, abandoned: 0, proverRejected: 0 };
98
+ /** The most recent deterministic failure; see {@link RunSummary.lastFailure}. */
99
+ lastFailure;
100
+ /**
101
+ * The last rejection of each item IN THIS RUN, kept for the repair attempt.
102
+ * In memory on purpose: the durable copy is the evidence directory, and a
103
+ * later run that picks the item up again reads `lastFailure` off the item —
104
+ * it does not restore a candidate from a run it did not see rejected.
105
+ */
106
+ rejections = new Map();
107
+ /**
108
+ * Set only on the path that ends the loop through {@link RoundRunnerDeps.haltRequested},
109
+ * and read only when writing `run.ended`. Held rather than threaded because
110
+ * `loop()` returns a stop reason and nothing else — widening that return to
111
+ * carry a payload only one of its eight answers can have would put the
112
+ * exception in the type every other caller reads.
113
+ */
114
+ halt = null;
115
+ /** F29: the commit this run was cut from, set once, before the first round. */
116
+ cutBase = '';
117
+ /**
118
+ * Consecutive rounds whose gate could not run. Reset by any gate that reached
119
+ * a verdict, so it counts a machine that is broken now rather than one that
120
+ * was ever briefly unhealthy.
121
+ */
122
+ consecutiveBlocked = 0;
123
+ /**
124
+ * Whether the round now running ended blocked.
125
+ *
126
+ * ⚠️ THE RESET LIVES IN THE LOOP, NOT IN THE ROUND, and that is the whole
127
+ * point of this second field. `runRound` has six exits before the gate — an
128
+ * implementer throw, `abandoned`, a railed path, a no-op, an external halt —
129
+ * and a reset placed anywhere inside it is a reset those exits skip. The
130
+ * counter would then read three blocked gates with ordinary rounds between
131
+ * them as "three in a row", and the halt sentence would be a false statement
132
+ * about the machine.
133
+ */
134
+ blockedThisRound = false;
135
+ /** See {@link RunSummary.metUnavailableEnvironment}. */
136
+ metUnavailableEnvironment = false;
137
+ constructor(cfg, deps, id) {
138
+ this.cfg = cfg;
139
+ this.deps = deps;
140
+ this.id = id;
141
+ // Wiring mismatches surface here, not at hour three of an overnight run:
142
+ // proof enabled with no prover would discard (or worse, commit) every round,
143
+ // and `on_partial: ask` with nobody to ask has no honest fallback.
144
+ if (cfg.proof.enabled && !deps.prove) {
145
+ throw new RunnerError('proof.enabled is true but no prover is wired');
146
+ }
147
+ if (cfg.proof.enabled && cfg.proof.onPartial === 'ask' && !deps.resolvePartial) {
148
+ throw new RunnerError("proof.on_partial is 'ask' but nothing is wired to ask");
149
+ }
150
+ if (deps.exemptionsFor && !deps.spendExemption) {
151
+ throw new RunnerError('exemptionsFor is wired but no spendExemption sink — grants would never be spent');
152
+ }
153
+ if (deps.reverify && !deps.evidence) {
154
+ // §12 case 19's second trigger depends on the discard invalidating the
155
+ // cache. Without it a discarded round leaves a `hit` behind, and the next
156
+ // pick would reuse the exact evidence that just led into a wall.
157
+ throw new RunnerError('reverify is wired but no evidence.invalidate — a discard would leave stale evidence hittable');
158
+ }
159
+ }
160
+ async run() {
161
+ // Set here rather than as a field initialiser: `deps` is a constructor
162
+ // parameter property and is not initialised when field initialisers run.
163
+ if (this.deps.perRoundSafety === true)
164
+ this.roundFacts = [];
165
+ const dirty = await this.deps.git.changedPaths();
166
+ if (dirty.length > 0) {
167
+ // §12 case 1: a dirty tree at start would fold uncommitted human work into
168
+ // round 1's diff — and the first discard would destroy it.
169
+ throw new RunnerError(`working tree has ${dirty.length} changed path(s) — refusing to start (commit or stash first)`);
170
+ }
171
+ /**
172
+ * WHERE THIS RUN STARTS, WHEN SOMETHING HAS AN OPINION ABOUT IT.
173
+ *
174
+ * ⚠️ THE RUNNER STILL KNOWS NOTHING ABOUT INTEGRATION. It asks the
175
+ * composition root for a start point and is handed one or not — the same
176
+ * arrangement as every other seam here. Absent means "wherever HEAD is",
177
+ * which is what every run has always done.
178
+ */
179
+ const cutFromRef = (await this.deps.integrationBase?.()) ?? undefined;
180
+ await this.deps.git.createBranch(this.id.branch, cutFromRef);
181
+ const baseSha = await this.deps.git.headSha();
182
+ this.cutBase = baseSha;
183
+ if (cutFromRef !== undefined) {
184
+ // Both halves recorded on the ledger the run already owns: what was
185
+ // chosen, and what the run actually got.
186
+ await this.emit('alert', {
187
+ code: 'run_cut_from',
188
+ severity: 'info',
189
+ text: `cut from ${cutFromRef} at ${baseSha}`,
190
+ });
191
+ }
192
+ const startedAt = this.deps.now();
193
+ await this.emit('run.started', {
194
+ // Spread FIRST, on purpose. Whatever the composition root supplied is
195
+ // overwritten by the six authoritative values below, so context can add to
196
+ // the header but can never lie about the run's own identity.
197
+ ...this.deps.runContext,
198
+ runId: this.id.runId,
199
+ branch: this.id.branch,
200
+ baseSha,
201
+ minHours: this.cfg.run.minHours,
202
+ maxRounds: this.cfg.run.maxRounds,
203
+ lane: this.id.lane,
204
+ });
205
+ await this.deps.notifier?.runStarted({ minHours: this.cfg.run.minHours, baseSha });
206
+ // A control request belongs to the run it was aimed at. Clearing it here
207
+ // means last night's pause cannot wedge tonight's run before its first
208
+ // round — a hold nobody remembers setting is indistinguishable from a hang.
209
+ //
210
+ // ONLY WHEN BEGINNING THIS RUN WAS ITSELF A PERSON SAYING GO. That was the
211
+ // stated justification and it was applied unconditionally, which was
212
+ // correct while every run began with a tap. A run this machine decided to
213
+ // begin has no such authority: erasing a pause a person set would be the
214
+ // machine overruling them with an act they never performed.
215
+ if (this.deps.consumeControlOnStart !== false)
216
+ await this.deps.control?.consume();
217
+ let stopReason;
218
+ try {
219
+ stopReason = await this.loop(startedAt);
220
+ }
221
+ catch (e) {
222
+ // The loop threw where nothing caught it — a git failure, a ledger write
223
+ // that could not land, a queue write that lost its retry. Before this the
224
+ // run simply stopped: no `run.ended`, so `status.json` said `live` for
225
+ // ever, the phone showed a run that never finished, and the server's Run
226
+ // row never closed.
227
+ //
228
+ // `aborted` rather than any ordinary reason, because this run did not end
229
+ // its own loop. The totals are still exact — this process counted them —
230
+ // which is what separates it from the `crashed` a later process writes on
231
+ // a dead run's behalf.
232
+ //
233
+ // Emitting is best-effort and deliberately swallowed: if the ledger is
234
+ // what threw, trying to write the ending would replace the real error
235
+ // with a confusing one.
236
+ const elapsedMs = this.deps.now() - startedAt;
237
+ try {
238
+ await this.emit('alert', {
239
+ code: 'run-aborted',
240
+ severity: 'critical',
241
+ text: `the run stopped on an error: ${e.message}`,
242
+ });
243
+ await this.emit('run.ended', {
244
+ runId: this.id.runId,
245
+ elapsedMs,
246
+ stopReason: 'aborted',
247
+ totals: this.totals,
248
+ });
249
+ }
250
+ catch {
251
+ /* the ledger is the likeliest thing to have thrown; keep the original */
252
+ }
253
+ throw e;
254
+ }
255
+ const elapsedMs = this.deps.now() - startedAt;
256
+ /**
257
+ * A RUN THAT HALTS ITSELF SAYS WHY (M6-12) — one `alert` immediately
258
+ * before `run.ended`, carrying the halt's own sentence, exactly as the
259
+ * crash path above writes `run-aborted`. Only a halt: an ordinary ending
260
+ * (`command`, `max-rounds`, `queue-empty`, …) has nothing to explain.
261
+ * Best-effort like the crash path's: a ledger that refuses the alert must
262
+ * not stop the ending being written — and if the ledger is dead, the
263
+ * `run.ended` below says so by throwing where it always has.
264
+ */
265
+ if (this.halt !== null && this.halt.because.length > 0) {
266
+ try {
267
+ await this.emit('alert', {
268
+ code: exports.RUN_HALTED_CODE,
269
+ severity: this.halt.stopReason === 'aborted' ? 'warn' : 'info',
270
+ text: this.halt.because,
271
+ });
272
+ }
273
+ catch {
274
+ /* the ending below is the record; the alert was the courtesy */
275
+ }
276
+ }
277
+ await this.emit('run.ended', {
278
+ // Spread FIRST, for the reason `run.started` spreads `runContext` first:
279
+ // the caller may add attribution to the ending and can never restate the
280
+ // run's own facts, because the four below are written over whatever it
281
+ // sent.
282
+ ...this.halt?.endedContext,
283
+ runId: this.id.runId,
284
+ elapsedMs,
285
+ stopReason,
286
+ totals: this.totals,
287
+ });
288
+ await this.deps.notifier?.runEnded({ totals: this.totals, elapsedMs, branch: this.id.branch, stopReason });
289
+ const summary = { stopReason, totals: this.totals, elapsedMs };
290
+ if (this.roundFacts !== null)
291
+ summary.rounds = this.roundFacts;
292
+ if (this.refusedBeforeFirstRound !== null)
293
+ summary.refusedBeforeFirstRound = this.refusedBeforeFirstRound;
294
+ if (this.surfaceViolation !== null)
295
+ summary.surfaceViolation = this.surfaceViolation;
296
+ if (this.metUnavailableEnvironment)
297
+ summary.metUnavailableEnvironment = true;
298
+ // Only when there was one: an absent field says "no deterministic failure",
299
+ // which is different from "a failure nobody could identify".
300
+ if (this.lastFailure !== undefined)
301
+ summary.lastFailure = this.lastFailure;
302
+ if (this.failedAtThisRound !== null)
303
+ summary.lastFailedAt = this.failedAtThisRound;
304
+ return summary;
305
+ }
306
+ /**
307
+ * One record per round, or null when nothing asked for them.
308
+ *
309
+ * ⚠️ NULL RATHER THAN EMPTY, so "nobody asked" and "no rounds happened" stay
310
+ * different answers. A caller handed `[]` cannot tell which it got.
311
+ */
312
+ roundFacts = null;
313
+ /** Why `beforeFirstRound` said no, so `run()` can put it on the summary. */
314
+ refusedBeforeFirstRound = null;
315
+ /** Set when a round wrote outside what this run declared. */
316
+ surfaceViolation = null;
317
+ /**
318
+ * Where THIS round failed. Reset at the top of every round, like
319
+ * `blockedThisRound` — a latched value would sign round N+1 with round N's
320
+ * failure, which is the shape that makes a detector fire on one real failure
321
+ * repeated by the recorder.
322
+ */
323
+ failedAtThisRound = null;
324
+ /** Gate steps that passed in THIS round. Reset with everything else per-round. */
325
+ passingGatesThisRound = [];
326
+ /**
327
+ * The implementer's sentence about the round in flight, on its way to the
328
+ * ledger (DESIGN §9, `round.ended.note`).
329
+ *
330
+ * HELD ON THE RUNNER RATHER THAN THREADED THROUGH, because every one of the
331
+ * seven ways a round can end goes through `endRound` and the note belongs on
332
+ * all of them — most of all on the ones that threw the work away, where it is
333
+ * the only record of what was attempted. A parameter would have to be added to
334
+ * each exit and the next exit somebody writes would silently omit it.
335
+ *
336
+ * ⚠️ CLEARED AT THE TOP OF EVERY ROUND. A stale note is worse than none: it
337
+ * would attribute one round's account of itself to the next round's outcome.
338
+ */
339
+ roundNote;
340
+ /** What this round’s repair was, for `round.ended`. Reset with {@link roundNote}. */
341
+ roundRepair;
342
+ /**
343
+ * WHAT THIS ROUND DID, FROM THE DIFFERENCE THE ROUND MADE.
344
+ *
345
+ * 🔴 THREE FIELDS ARE RUN-LEVEL ROLLUPS AND MUST NOT BE READ DIRECTLY.
346
+ *
347
+ * `this.lastFailure` is assigned when a failure happens and NEVER cleared, so
348
+ * reading it per round signs round N+1's record with round N's failure — the
349
+ * exact shape that would make `repeated_failure` fire on ONE real failure
350
+ * repeated by the recorder rather than by the machine. Same for
351
+ * `metUnavailableEnvironment`, which latches true for the rest of the run,
352
+ * and `totals.committed`, which only grows. The environment flag is read
353
+ * from `blockedThisRound`, which IS per-round, rather than compared.
354
+ *
355
+ * So each is compared against its value BEFORE the round: what changed during
356
+ * this round is this round's, and what did not is not.
357
+ */
358
+ async recordRoundFact(round, committedBefore, failureBefore) {
359
+ if (this.roundFacts === null)
360
+ return;
361
+ const worked = this.totals.committed > committedBefore;
362
+ /**
363
+ * ⚠️ AND THE TREE IS ASKED FOR ONCE PER ROUND, NOT DERIVED. `oscillation`
364
+ * compares four consecutive fingerprints and returns null if any is null,
365
+ * so a fingerprint this cannot obtain switches the detector off rather than
366
+ * weakening it — which is why a failure here is recorded as null and the
367
+ * caller is left to see a run whose rounds carry no tree.
368
+ */
369
+ const treeFingerprint = this.deps.git.headTreeSha === undefined ? null : await this.deps.git.headTreeSha().catch(() => null);
370
+ this.roundFacts.push({
371
+ round,
372
+ worked,
373
+ failedAt: worked ? null : this.failedAtThisRound,
374
+ passingGates: this.passingGatesThisRound,
375
+ treeFingerprint,
376
+ failure: this.lastFailure !== failureBefore ? (this.lastFailure ?? null) : null,
377
+ /**
378
+ * 🔴 `blockedThisRound`, NOT THE LATCHED RUN-LEVEL FLAG.
379
+ *
380
+ * This was `this.metUnavailableEnvironment && !metBefore`, which is true
381
+ * for the FIRST blocked round and false for every one after it, because
382
+ * the run-level flag latches and never clears. MEASURED: with containment
383
+ * broken from round 2 onward, rounds 3 and 4 were recorded as ordinary
384
+ * `gate_failed` attempts — two observations per run asserting the MODEL
385
+ * achieved nothing, for a fault in the MACHINE. That is precisely the
386
+ * poisoning the `blocked` verdict exists to prevent.
387
+ *
388
+ * `blockedThisRound` is reset at the top of every round, so it answers
389
+ * about this round and nothing else.
390
+ */
391
+ metUnavailableEnvironment: this.blockedThisRound,
392
+ work: this.deps.queue.list(),
393
+ });
394
+ }
395
+ /** The round loop itself, so `run()` can put an honest ending on a throw. */
396
+ async loop(startedAt) {
397
+ for (let round = 1;; round++) {
398
+ // A signal is checked first: when a human killed the run, `run.ended`
399
+ // must say `signal`, not claim a budget limit happened to expire at the
400
+ // same moment (§12 case 11). Like the budget checks below it sits between
401
+ // rounds, so the in-flight round finishes before the run winds down.
402
+ if (this.deps.stopRequested?.())
403
+ return 'signal';
404
+ // AND BEFORE THE PICK, not only after the model turn. `pickNext` can
405
+ // route an item back through verification, which is a second full model
406
+ // invocation — so a halt armed by anything other than this loop's own
407
+ // last round would otherwise pay for one more before noticing. Nothing
408
+ // arms it that way today; the fence is here because the day something
409
+ // does, this is where the cost would land.
410
+ const armed = this.deps.haltRequested?.() ?? null;
411
+ if (armed !== null) {
412
+ this.halt = armed;
413
+ return armed.stopReason;
414
+ }
415
+ // The phone's pause/stop, read from control.json. It sits next to the
416
+ // signal check and for the same reason: both are a human's intent, and
417
+ // `run.ended` must say `command` rather than name whichever budget limit
418
+ // happened to expire at the same moment. Being here also means the
419
+ // in-flight round finishes first, exactly like a budget limit.
420
+ const control = await this.awaitControl(startedAt);
421
+ if (control !== 'continue')
422
+ return control;
423
+ // Budget checks sit between rounds, never inside one: a limit reached
424
+ // mid-round finishes that round first (§12 case 10).
425
+ if (this.totals.rounds >= this.cfg.run.maxRounds)
426
+ return 'max-rounds';
427
+ if (this.deps.now() - startedAt >= this.cfg.run.minHours * 3_600_000)
428
+ return 'min-hours';
429
+ const item = await this.pickNext();
430
+ if (item === undefined)
431
+ return 'queue-empty';
432
+ /**
433
+ * ⚠️ AFTER `pickNext`, NOT BEFORE. An empty work set ends the run at zero
434
+ * rounds, and a caller that had already spent something by then spent it
435
+ * on nothing. This is the first line at which a round is certain.
436
+ */
437
+ if (round === 1 && this.deps.beforeFirstRound !== undefined) {
438
+ const may = await this.deps.beforeFirstRound();
439
+ if (!may.ok) {
440
+ this.refusedBeforeFirstRound = may.because ?? 'the first round was refused';
441
+ // A reasoned halt like any other: `run()` says why before it ends.
442
+ this.halt = { stopReason: 'aborted', because: this.refusedBeforeFirstRound };
443
+ return 'aborted';
444
+ }
445
+ }
446
+ this.blockedThisRound = false;
447
+ this.failedAtThisRound = null;
448
+ this.passingGatesThisRound = [];
449
+ const committedBefore = this.totals.committed;
450
+ const failureBefore = this.lastFailure;
451
+ const ended = await this.runRound(round, item);
452
+ // ⚠️ BEFORE `consecutiveBlocked` IS RESET, because `blockedThisRound` is
453
+ // what the record reads and the line below does not touch it.
454
+ await this.recordRoundFact(round, committedBefore, failureBefore);
455
+ /**
456
+ * A round that wrote outside its lease ends the RUN, not just the round.
457
+ * The round itself was already discarded where the trespass was seen —
458
+ * before the gate, before any commit. See `runRound`.
459
+ */
460
+ if (this.surfaceViolation !== null) {
461
+ // The same facts the item's own failure line carries, so the ending
462
+ // and the item say one thing; `run()` writes it before `run.ended`.
463
+ const { paths, declared } = this.surfaceViolation;
464
+ this.halt = {
465
+ stopReason: 'aborted',
466
+ because: `a round wrote outside the surfaces this run declared (${declared.join(', ')}): ` +
467
+ `${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''} — the run ends here`,
468
+ };
469
+ return 'aborted';
470
+ }
471
+ if (!this.blockedThisRound)
472
+ this.consecutiveBlocked = 0;
473
+ this.totals.rounds += 1;
474
+ const soFar = this.deps.now() - startedAt;
475
+ await this.deps.notifier?.roundFinished({
476
+ outcome: ended.outcome,
477
+ itemTitle: ended.title,
478
+ elapsedMs: soFar,
479
+ remainingMs: Math.max(0, this.cfg.run.minHours * 3_600_000 - soFar),
480
+ });
481
+ // AFTER the round is counted and reported, and BEFORE the cooldown. The
482
+ // round genuinely happened and belongs in the totals; the pause exists to
483
+ // space out the next one, and there is no next one.
484
+ if (ended.halt) {
485
+ this.halt = ended.halt;
486
+ return ended.halt.stopReason;
487
+ }
488
+ /**
489
+ * ⚠️ AFTER THE ROUND IS COUNTED AND REPORTED. The round genuinely
490
+ * happened and belongs in the totals; what has changed is that there
491
+ * should not be another one.
492
+ */
493
+ if (this.deps.baseStale !== undefined) {
494
+ const because = await this.deps.baseStale(this.cutBase);
495
+ if (because !== null) {
496
+ this.halt = { stopReason: 'aborted', because };
497
+ return 'aborted';
498
+ }
499
+ }
500
+ if (this.cfg.run.cooldownS > 0 && this.deps.sleep) {
501
+ await this.deps.sleep(this.cfg.run.cooldownS * 1000);
502
+ }
503
+ }
504
+ }
505
+ /**
506
+ * Consult the control file, holding here for as long as it says `pause`.
507
+ *
508
+ * Returns `continue` to start the next round, or the reason the run should
509
+ * wind down. `stop` ends the run; `pause` keeps it alive on its branch with
510
+ * its queue intact, so resuming continues the same run instead of paying for
511
+ * a fresh one — that is the whole difference between the two.
512
+ */
513
+ async awaitControl(startedAt) {
514
+ const control = this.deps.control;
515
+ if (!control)
516
+ return 'continue';
517
+ let announced = false;
518
+ for (;;) {
519
+ const req = await control.read();
520
+ if (req === null || req.kind === 'resume') {
521
+ // Only when this run actually held. `resume` is also the cleared state
522
+ // of the control file, so it is the ordinary reading between rounds —
523
+ // announcing it unconditionally would put a line in the ledger for
524
+ // every round of every run, into an alerts array nothing trims.
525
+ if (announced) {
526
+ await this.emit('alert', {
527
+ code: 'run-resumed',
528
+ severity: 'info',
529
+ text: `resumed${req?.actor ? ` by ${req.actor}` : ''} — picking up the next round`,
530
+ });
531
+ }
532
+ return 'continue';
533
+ }
534
+ if (req.kind === 'stop') {
535
+ /**
536
+ * NOT CONSUMED — BY ANY RUN (M6-14). A person's own run used to clear
537
+ * the file the instant it acted on it, and the reason given was that a
538
+ * stop left on disk would end the person's next `looply run` before
539
+ * its first round — which stopped being true the day a person's run
540
+ * began consuming the file on START (`consumeControlOnStart`, above):
541
+ * their next Start clears it before its first poll. What the clear
542
+ * still did was speak for them to the autonomy tick. Found live
543
+ * (M6-13): the stand-in's stop ended the placed run, the run wrote
544
+ * `resume` over it, and the next tick found no hold and reserved a
545
+ * Start for the pursuit it was still permitted; only the cancel's timing
546
+ * prevented a machine run.
547
+ *
548
+ * An autonomous run never consumed it (nobody is standing there, and
549
+ * wiping the instruction is how a tap meant to stop the night came to
550
+ * stop one run out of eight). Now no run does. The run still stops;
551
+ * the instruction stays where the person left it, for the tick to
552
+ * hold on, until a person's own Start or resume says otherwise.
553
+ */
554
+ return 'command';
555
+ }
556
+ // A hold that cannot sleep is a spin, so a runner with no sleep wired
557
+ // treats pause as the end of the run rather than burning a core.
558
+ if (!this.deps.sleep)
559
+ return 'command';
560
+ if (!announced) {
561
+ announced = true;
562
+ // There is no pause event in the schema, and a run that simply stops
563
+ // emitting looks identical to a dead one from the phone. This is the
564
+ // difference between "paused" and "hung".
565
+ await this.emit('alert', {
566
+ code: 'run-paused',
567
+ severity: 'info',
568
+ text: `paused${req.actor ? ` by ${req.actor}` : ''} — resume from the app to continue this run`,
569
+ });
570
+ }
571
+ // A paused run still answers Ctrl-C and still ends at its budget: a hold
572
+ // only the phone can lift is a wedged process the moment the phone is flat.
573
+ if (this.deps.stopRequested?.())
574
+ return 'signal';
575
+ if (this.deps.now() - startedAt >= this.cfg.run.minHours * 3_600_000)
576
+ return 'min-hours';
577
+ await this.deps.sleep(this.deps.pausePollMs ?? exports.DEFAULT_PAUSE_POLL_MS);
578
+ }
579
+ }
580
+ /** Topmost pickable item. Order IS priority (core `Item`), so the sort is the pick. */
581
+ async topPickable() {
582
+ /**
583
+ * `pickableItems` replaces the bare `isPickable` filter and does the same job
584
+ * plus one more: an item whose prerequisites are not satisfied never reaches
585
+ * the sort. That ordering is the enforcement — a priority cannot promote an
586
+ * item past an unmet prerequisite, because such an item is not in the list.
587
+ *
588
+ * ⚠️ THE SORT IS UNCHANGED. An item that names no prerequisites, which is
589
+ * every item written before `after` existed, is picked exactly as before.
590
+ *
591
+ * The same sentence now covers an item's cross-repo needs: one that declares
592
+ * none is picked exactly as before, and one that declares an unmet need is
593
+ * not in the list. A blocked sibling therefore delays nothing but the work
594
+ * that actually depends on it.
595
+ */
596
+ return (0, core_1.pickableItems)(this.deps.queue.list(), await this.deps.crossRepo?.()).sort((a, b) => a.orderKey < b.orderKey ? -1 : a.orderKey > b.orderKey ? 1 : 0)[0];
597
+ }
598
+ /**
599
+ * The pick, routed through the re-verification triggers (§12 case 19): the
600
+ * topmost item is handed to a round only when `decideReverify` judges its
601
+ * cached evidence fresh. A `reverify` decision — HEAD moved past the threshold,
602
+ * or no cache entry at all, which is the post-discard and never-verified state
603
+ * — sends the item back through verification instead, and the queue is re-read
604
+ * because the verdict may have routed the item out of `ready`, promoting the
605
+ * next one.
606
+ */
607
+ async pickNext() {
608
+ const verified = new Set();
609
+ for (;;) {
610
+ const top = await this.topPickable();
611
+ if (top === undefined || this.deps.reverify === undefined)
612
+ return top;
613
+ // Already verified within this pick: the premise is current even when the
614
+ // verdict carried nothing to cache (an injected verifier is not forced
615
+ // through the parser's evidence check) — asking again would loop forever
616
+ // on the same answer.
617
+ if (verified.has(top.id))
618
+ return top;
619
+ const head = await this.deps.git.headSha();
620
+ const decision = await (0, reverify_1.decideReverify)(this.deps.reverify.cache.lookup(top.id, head), head, this.deps.reverify.countCommitsTouching);
621
+ if (decision.kind === 'reuse')
622
+ return top;
623
+ verified.add(top.id);
624
+ await this.reverifyItem(top, head);
625
+ }
626
+ }
627
+ /**
628
+ * Send an item back through verification and apply the verdict to the queue
629
+ * (§10, §12 cases 17–19). Confirmed/amended refills the evidence cache at the
630
+ * HEAD the verifier judged against, so the next lookup is a hit; every other
631
+ * verdict routes the item out of `ready` and the pick moves on.
632
+ */
633
+ async reverifyItem(picked, head) {
634
+ const routing = this.deps.reverify;
635
+ if (routing === undefined)
636
+ throw new RunnerError('reverifyItem called without reverify wired');
637
+ const item = await this.patchItem(picked.id, { state: 'verifying' });
638
+ const outcome = await routing.verify(item);
639
+ // Shared with the verification pass over a scan's drafts (verify-apply.ts),
640
+ // so an item becoming pickable never depends on which route reached it.
641
+ const decided = (0, verify_apply_1.applyVerifyVerdict)(item, outcome, (id) => this.deps.queue.get(id));
642
+ await this.emit('item.verified', { itemId: item.id, ...decided.outcome });
643
+ await this.patchItem(item.id, decided.patch);
644
+ if (decided.cacheEvidence) {
645
+ await routing.cache.put(item.id, head, decided.cacheEvidence);
646
+ }
647
+ }
648
+ /**
649
+ * Returns how the round ended plus the item's title, for the notifier — and
650
+ * the halt, when this round is also the last one.
651
+ */
652
+ async runRound(round, picked) {
653
+ const attempt = picked.attempts + 1;
654
+ let state = 'picked';
655
+ // Before anything can end this round. See {@link roundNote}.
656
+ this.roundNote = undefined;
657
+ this.roundRepair = undefined;
658
+ await this.emit('round.picked', { round, itemId: picked.id, attempt, title: picked.title });
659
+ const item = await this.patchItem(picked.id, { state: 'in_progress' });
660
+ state = (0, core_1.transition)(state, 'implementing');
661
+ await this.emit('round.phase', { round, phase: 'implementing' });
662
+ /**
663
+ * BEFORE THE PAID TURN, AND AFTER THE TRANSITION TO `implementing`.
664
+ *
665
+ * Both halves of that position are load-bearing. Before, because the whole
666
+ * point is not to buy a model turn a broken machine cannot use. After,
667
+ * because `TRANSITIONS` allows `implementing → discarded` and does NOT
668
+ * allow `picked → discarded`; moving this one line up throws
669
+ * `RoundTransitionError` and takes the run down.
670
+ *
671
+ * The blocked machinery is reused verbatim rather than re-decided: no
672
+ * attempt spent, item back to `ready` rather than `closed`, no failure
673
+ * signature, no `gate.result` and so no false `gate_failed`, the same bound
674
+ * of three, and `metUnavailableEnvironment` reaching the unchanged exclusion in
675
+ * `run-cmd.ts`.
676
+ */
677
+ /**
678
+ * WRAPPED, because this was the ONE un-try'd await in `runRound` — sitting
679
+ * directly above the `implement` call that IS wrapped, for the reason the
680
+ * comment there gives. The preflight closure can throw: `containedRoot()`
681
+ * does an unguarded `mkdirSync`, and the backend's own `mkdtempSync` sits
682
+ * outside its `try`. A full or locked `%TEMP%` would therefore take the run
683
+ * down with no `run.ended`, strand the item `in_progress`, and let recovery
684
+ * spend an attempt — the exact inversion of what this check is for.
685
+ */
686
+ let ready;
687
+ try {
688
+ ready = await this.deps.environmentReady?.();
689
+ }
690
+ catch (err) {
691
+ ready = {
692
+ ok: false,
693
+ unavailable: true,
694
+ timedOut: false,
695
+ ms: 0,
696
+ output: `the environment check itself failed: ${err.message}`,
697
+ };
698
+ }
699
+ if (ready?.unavailable === true) {
700
+ const why = 'round blocked: a container could not be made — the environment is unavailable, ' +
701
+ 'not the change. No attempt was spent';
702
+ this.blockedThisRound = true;
703
+ this.consecutiveBlocked += 1;
704
+ /**
705
+ * ⚠️ ON THE FIRST BLOCKED ROUND, NOT AT THE BOUND. Setting this only when
706
+ * the run aborts left three other endings reachable first — `max-rounds`
707
+ * (which config allows as low as 1), `queue-empty`, `min-hours` — and each
708
+ * of those is then recorded as `gate_failed` with an unchanged tree
709
+ * fingerprint, which is what feeds `noProgress`. The bound governs the
710
+ * ABORT; the exclusion governs any run that met an unavailable
711
+ * environment, whenever it happens to stop.
712
+ */
713
+ this.metUnavailableEnvironment = true;
714
+ state = (0, core_1.transition)(state, 'discarded');
715
+ const outcome = await this.discardAndReturn(round, state, item.id, why, false);
716
+ if (this.consecutiveBlocked >= exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS) {
717
+ return {
718
+ outcome,
719
+ title: item.title,
720
+ halt: {
721
+ stopReason: 'aborted',
722
+ endedContext: { blockedReason: why },
723
+ because: `${exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS} rounds in a row could not make a container — ` +
724
+ 'nothing was charged to the queue; fix the machine and start again',
725
+ },
726
+ };
727
+ }
728
+ return { outcome, title: item.title };
729
+ }
730
+ /**
731
+ * A repair attempt starts from what the last one left, explained. Built
732
+ * BEFORE the model is invoked and after the environment check, so a
733
+ * restored candidate is never put into a tree no container can gate.
734
+ */
735
+ const repair = await this.prepareRepair(item, attempt);
736
+ let impl;
737
+ try {
738
+ impl = await this.deps.implement(item, attempt, repair);
739
+ }
740
+ catch (err) {
741
+ // The prover-error's sibling: `runImplementer` already returns `abandoned`
742
+ // for a timeout or an unparseable result, so a *throw* here is the process
743
+ // itself failing (spawn ENOENT, transport down) — and it used to escape
744
+ // runRound and kill the whole overnight run. The round ends `abandoned`
745
+ // with the tree discarded, but the item goes back to `ready`, not
746
+ // `closed`: the crash says nothing about the item, and closing it would
747
+ // cost the item as well as the round. Critical alert for the same reason
748
+ // as the prover's — a dead agent binary abandons every round until
749
+ // someone hears.
750
+ const why = err instanceof Error ? err.message : String(err);
751
+ await this.emit('alert', {
752
+ code: 'implementer-error',
753
+ severity: 'critical',
754
+ text: `implementer invocation failed: ${why}`,
755
+ });
756
+ state = (0, core_1.transition)(state, 'abandoned');
757
+ const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `implementer error: ${why}`, undefined, { stage: 'implementer' });
758
+ return { outcome, title: item.title };
759
+ }
760
+ /**
761
+ * ASKED BEFORE THE RESULT IS READ, and that ordering is the invariant.
762
+ *
763
+ * The turn came back across a boundary the composition root recognises and
764
+ * this loop does not. Everything after this line — the gate, the commit
765
+ * permit, the prover, the commit itself — is work done on the far side of a
766
+ * point where stopping was the answer, so none of it happens.
767
+ *
768
+ * `discarded`, and it is a statement about the ROUND'S LOCAL WORK rather
769
+ * than about why the run stopped. Nothing here passed a gate and nothing was
770
+ * proven, and un-gated work has never been kept by any path in this file;
771
+ * calling it `abandoned` would additionally say the item taught us nothing
772
+ * and close it, which is false — the item is exactly as workable as it was,
773
+ * and the next attempt should have it back.
774
+ */
775
+ const halt = this.deps.haltRequested?.() ?? null;
776
+ if (halt !== null) {
777
+ state = (0, core_1.transition)(state, 'discarded');
778
+ const outcome = await this.discardAndReturn(round, state, item.id, halt.because);
779
+ return { outcome, title: item.title, halt };
780
+ }
781
+ if (impl.kind === 'abandoned') {
782
+ // §12 case 4. The tree is cleaned, never consulted: §2.4 forbids deciding
783
+ // from it, but a dead agent's half-written edits must not leak into the
784
+ // next round's diff. `closed`, not `ready`: nothing was learned, and a
785
+ // requeue here is the "same item forever" loop.
786
+ await this.discardTree(item.id);
787
+ state = (0, core_1.transition)(state, 'abandoned');
788
+ const outcome = await this.endRound(round, state, item.id, { state: 'closed', lastFailure: impl.reason });
789
+ return { outcome, title: item.title };
790
+ }
791
+ const result = impl.result;
792
+ // From here every exit is an `endRound`, and every one of them carries this.
793
+ this.roundNote = (0, core_1.normalizeRoundNote)(result.note);
794
+ if (result.outcome === 'blocked') {
795
+ // §8: emit the exemption request and move to the next item. The runner
796
+ // never waits for the phone — the question outlives the round.
797
+ const blocked = result.blocked ?? { path: '(unknown)', reason: 'blocked with no details' };
798
+ // 🔴 `rails.exemptions: 'never'` MEANS IT DOES NOT ASK — and until now
799
+ // nothing branched on it. The setting was parsed, validated as one of two
800
+ // words, and offered on the phone, while an agent that reported a block
801
+ // produced this request under either value. Somebody who had said "never
802
+ // ask me" was asked anyway.
803
+ //
804
+ // ⚠️ IT STILL BLOCKS. `never` is about the QUESTION, not the outcome: the
805
+ // item is blocked either way, the tree is discarded either way, and the
806
+ // run picks the next item either way. Suppressing `item.blocked` too
807
+ // would turn a preference about interruptions into silently losing work.
808
+ if (this.cfg.rails.exemptions === 'ask') {
809
+ await this.emit('exemption.requested', {
810
+ itemId: item.id,
811
+ path: blocked.path,
812
+ reason: blocked.reason,
813
+ });
814
+ }
815
+ await this.emit('item.blocked', { itemId: item.id, reason: `${blocked.path}: ${blocked.reason}` });
816
+ await this.discardTree(item.id);
817
+ state = (0, core_1.transition)(state, 'abandoned');
818
+ const outcome = await this.endRound(round, state, item.id, { state: 'blocked', blockedReason: blocked.reason });
819
+ return { outcome, title: item.title };
820
+ }
821
+ if (result.outcome === 'no-op') {
822
+ // §2.4: "nothing to do" is a statement the agent makes; the tree is not
823
+ // asked for a second opinion. The discard clears any stray writes anyway.
824
+ await this.discardTree(item.id);
825
+ state = (0, core_1.transition)(state, 'abandoned');
826
+ const outcome = await this.endRound(round, state, item.id, { state: 'closed' });
827
+ return { outcome, title: item.title };
828
+ }
829
+ state = (0, core_1.transition)(state, 'gating');
830
+ await this.emit('round.phase', { round, phase: 'gating' });
831
+ const changed = await this.deps.git.changedPaths();
832
+ /**
833
+ * 🔴 A DECLARATION IS A HOPE; THIS LIST IS FACT — AND IT IS CHECKED HERE,
834
+ * BEFORE THE GATE AND BEFORE ANY COMMIT.
835
+ *
836
+ * The obvious place is after the round, and it is the wrong one twice over:
837
+ * a successful round has COMMITTED by then, so "discard the round" would
838
+ * mean rewriting a commit — the one thing this codebase never does — and
839
+ * `changedPaths` after a commit is empty, so the check would silently only
840
+ * ever see rounds that had already failed.
841
+ *
842
+ * Here the writes are still in the working tree, and the existing discard
843
+ * path applies unchanged.
844
+ *
845
+ * ⚠️ AND IT ENDS THE RUN RATHER THAN RETRYING THE ITEM. A run that wrote
846
+ * outside its lease has demonstrated its DECLARATION is wrong; a retry
847
+ * would run the same wrong declaration again.
848
+ */
849
+ const declaredSurfaces = this.deps.declaredSurfaces ?? [];
850
+ if (declaredSurfaces.length > 0) {
851
+ const trespass = (0, core_1.surfaceViolations)(changed, declaredSurfaces);
852
+ if (trespass.length > 0) {
853
+ const paths = trespass.map((v) => v.path);
854
+ this.surfaceViolation = { paths, declared: declaredSurfaces };
855
+ const why = `it wrote outside the surfaces it declared (${declaredSurfaces.join(', ')}): ` +
856
+ `${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''}`;
857
+ /**
858
+ * 🔴 THE TRANSITION, WHICH THIS SITE ALONE WAS MISSING — SO THE SAFETY
859
+ * CHECK'S OWN SUCCESS PATH THREW.
860
+ *
861
+ * `state` is `gating` here, `endRound` calls `outcomeOf(state)`, and a
862
+ * non-terminal state makes that null and throws `RunnerError: round
863
+ * ended in non-terminal state gating`. The throw happens BEFORE
864
+ * `patchItem`, so the item is never moved off `in_progress`: the item
865
+ * is stranded, the run dies with a stack trace instead of the sentence
866
+ * explaining the trespass, and the next run finds an item claimed by a
867
+ * process that is gone.
868
+ *
869
+ * MEASURED: a round writing one path outside its declared surface threw
870
+ * rather than discarding. Every other `discardAndReturn` caller — the
871
+ * environment block, the external halt, the blocked round — transitions
872
+ * first; this one was written last and did not.
873
+ */
874
+ state = (0, core_1.transition)(state, 'discarded');
875
+ const outcome = await this.discardAndReturn(round, state, item.id, why, false);
876
+ return { outcome, title: item.title };
877
+ }
878
+ }
879
+ // The declaration is a hope; the diff is fact (E-04). What the agent said
880
+ // it touched against what `git status` shows — new, untracked files
881
+ // included — goes to the front of the round's note, counts first, where a
882
+ // person reads it. Recorded, not acted on: the gate decides this round
883
+ // exactly as before. A truthful account adds nothing.
884
+ const touched = (0, core_1.describeTouched)((0, core_1.touchedVsChanged)(result.filesTouched, changed));
885
+ /**
886
+ * AND WHAT THIS ROUND DID TO THE THINGS THAT JUDGED IT (the audit's G-03).
887
+ *
888
+ * A round is kept or discarded by a gate it is permitted to edit — its spec
889
+ * files sit inside the scope it is told it may change, and `package.json` is
890
+ * on no rail — and until now the result said nothing about whether it had
891
+ * edited them. Same seam and the same rule as E-04 above: RECORDED, NOT
892
+ * ACTED ON. The gate decides this round exactly as before, adding a test is
893
+ * the point rather than a suspicion, and a round that touched nothing that
894
+ * judges it adds no characters at all.
895
+ *
896
+ * ⚠️ `changedEntries` CARRIES THE STATUS AND `changedPaths` DOES NOT, which is
897
+ * the difference between a test that was deleted and a test that was added —
898
+ * the two ends of what this exists to tell apart. It is optional on
899
+ * `RunnerGit` for the reason stated there, so a driver that cannot answer
900
+ * yields paths with no status and the sentence says `touched` rather than
901
+ * inventing one.
902
+ */
903
+ const statusesAreReal = this.deps.git.changedEntries !== undefined;
904
+ const entries = statusesAreReal
905
+ ? await this.deps.git.changedEntries()
906
+ : changed.map((path) => ({ path, status: '' }));
907
+ const judged = (0, core_1.describeJudged)((0, core_1.judgedBy)(entries, { steps: this.cfg.gates, ...exports.GATE_MACHINERY, matches: glob_1.matchesGlob }));
908
+ /**
909
+ * E-04 first, then G-03, then the implementer's own sentence. Both
910
+ * machine-made facts keep their counts ahead of the prose, where a
911
+ * truncation cannot hide them, and neither displaces the other; with
912
+ * nothing to report from either, this composes the note it composed before.
913
+ */
914
+ const facts = [touched, judged].filter((s) => s !== '').join(' — ');
915
+ if (facts !== '') {
916
+ this.roundNote = (0, core_1.normalizeRoundNote)(this.roundNote === undefined ? facts : `${facts} — ${this.roundNote}`);
917
+ }
918
+ // What each step printed, for the evidence of a rejection. Collected here
919
+ // and nowhere else: the result object stays digest-only by contract.
920
+ const diagnostics = [];
921
+ const gate = await (0, gate_1.runGate)({
922
+ steps: this.cfg.gates,
923
+ changedPaths: changed,
924
+ runner: this.deps.gateRunner,
925
+ diagnostics: (d) => diagnostics.push(d),
926
+ interlocks: {
927
+ changedPaths: changed,
928
+ rails: this.cfg.rails,
929
+ envValues: this.collectEnvValues(),
930
+ itemId: item.id,
931
+ now: this.deps.now(),
932
+ exemptions: this.deps.exemptionsFor?.(item.id) ?? [],
933
+ /**
934
+ * The scope interlock (ABORT_SCOPE, 93).
935
+ *
936
+ * 🔴 `changedEntries` IS PASSED ONLY WHEN THE STATUSES ARE REAL. A git
937
+ * driver that cannot report them yields `status: ''` for every path,
938
+ * and `''` is not a pure addition — so handing those over would abort
939
+ * every round under an additive grant, on the strength of a field the
940
+ * driver never filled in. An unmeasurable diff leaves the check unrun,
941
+ * which `checkScope` treats as absence of input rather than a finding.
942
+ */
943
+ ...(statusesAreReal ? { changedEntries: entries } : {}),
944
+ scope: this.cfg.scope,
945
+ },
946
+ });
947
+ /**
948
+ * A GATE THAT COULD NOT RUN IS NOT A GATE THAT WENT RED.
949
+ *
950
+ * `blocked` means the step never executed — no contained toolchain, or
951
+ * containment could not be made — so nothing about this item's code was
952
+ * measured. Charging it would spend an attempt and, three rounds later,
953
+ * close an item nothing ever examined.
954
+ *
955
+ * It also records no failure signature, which matters beyond this file: the
956
+ * signature is what a later reader uses to decide that the same failure
957
+ * keeps coming back, and a machine with no containment would otherwise
958
+ * produce a perfectly repeatable one on every item it touched.
959
+ */
960
+ const blocked = gate.verdict === 'blocked';
961
+ const blockedStep = blocked ? gate.steps.find((s) => !s.skipped && !s.ok) : undefined;
962
+ const blockedWhy = blocked
963
+ ? `gate blocked: step ${blockedStep?.step ?? '(unknown)'} could not run — ` +
964
+ `the environment is unavailable, not the change. No attempt was spent`
965
+ : undefined;
966
+ for (const s of gate.steps)
967
+ await this.emit('gate.step', { round, ...s });
968
+ const gateData = {
969
+ round,
970
+ // The wire never carries `blocked` (see `GateVerdict`): an older reader
971
+ // must see the red it would have seen, and `blockedReason` is the widened
972
+ // field that tells a newer one why nothing was charged.
973
+ verdict: blocked ? 'red' : gate.verdict,
974
+ steps: gate.steps,
975
+ };
976
+ if (gate.abort)
977
+ gateData.abortCode = gate.abort.code;
978
+ if (blockedWhy !== undefined)
979
+ gateData.blockedReason = blockedWhy;
980
+ await this.emit('gate.result', gateData);
981
+ if (blocked) {
982
+ this.blockedThisRound = true;
983
+ this.consecutiveBlocked += 1;
984
+ // Same rule as the preflight branch above: the exclusion is about having
985
+ // met an unavailable environment, not about how the run happened to end.
986
+ this.metUnavailableEnvironment = true;
987
+ state = (0, core_1.transition)(state, (0, core_1.afterGate)(gate.verdict));
988
+ // `discardAndReturn`, not `discardAndRequeue`: the tree is still cleaned,
989
+ // the reason still travels on the item, and the attempt is NOT spent —
990
+ // the same distinction an external halt already earns.
991
+ const outcome = await this.discardAndReturn(round, state, item.id, blockedWhy, false);
992
+ if (this.consecutiveBlocked >= exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS) {
993
+ /**
994
+ * BOUNDED, because "spends nothing" and "retries for ever" together are
995
+ * an unbounded loop against a machine that is not going to get better on
996
+ * its own. `aborted` is the honest existing reason — the run was alive
997
+ * and met a boundary it could not get over — and it keeps `stopReason`
998
+ * off the wire-change list.
999
+ */
1000
+ return {
1001
+ outcome,
1002
+ title: item.title,
1003
+ halt: {
1004
+ stopReason: 'aborted',
1005
+ /**
1006
+ * `endedContext` because `because` reaches nothing on this path:
1007
+ * it is read only where an EXTERNAL halt ends a round, and
1008
+ * `run.ended` spreads `endedContext` and nothing else. Without it
1009
+ * the operator sees three red gates and an unexplained abort.
1010
+ */
1011
+ endedContext: { blockedReason: blockedWhy },
1012
+ because: `${exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS} rounds in a row could not run their gate — ` +
1013
+ `${blockedStep?.step === undefined ? 'the environment is unavailable' : `step ${blockedStep.step} never executed`}. ` +
1014
+ `Nothing was charged to the queue; fix the machine and start again`,
1015
+ },
1016
+ };
1017
+ }
1018
+ return { outcome, title: item.title };
1019
+ }
1020
+ /**
1021
+ * ⚠️ RECORDED WHATEVER THE VERDICT. A gate that went green on three of five
1022
+ * steps and red on the fourth still proved those three, and a later round
1023
+ * that proves a different three is progress the fingerprint must be able to
1024
+ * see.
1025
+ */
1026
+ this.passingGatesThisRound = gate.steps.filter((s) => !s.skipped && s.ok).map((s) => s.step);
1027
+ if (gate.verdict !== 'green') {
1028
+ /**
1029
+ * ⚠️ RECORDED WHERE IT HAPPENED. `gate_failed` was the producer's default
1030
+ * for every non-committing round, which made it the answer for rounds
1031
+ * that never reached a gate at all. Setting it here means the word means
1032
+ * what the outcome list says it means: a deterministic gate refused it.
1033
+ */
1034
+ this.failedAtThisRound = 'gate';
1035
+ if (gate.verdict === 'abort') {
1036
+ // §12 cases 8–9: an abort is an alert, not just a tallied red — the gate
1037
+ // itself may be broken, and someone should hear tonight, not at review.
1038
+ await this.emit('alert', {
1039
+ code: 'gate-abort',
1040
+ severity: 'critical',
1041
+ text: gate.abort?.reason ?? 'interlock tripped',
1042
+ });
1043
+ }
1044
+ const failed = gate.steps.find((s) => !s.skipped && !s.ok);
1045
+ const failedStep = failed?.step;
1046
+ if (failed !== undefined) {
1047
+ /**
1048
+ * Held for the SUMMARY, not for this round's decision — the round is
1049
+ * already being discarded. Loop safety needs to know whether the same
1050
+ * failure keeps coming back, and only the runner sees the gate result.
1051
+ *
1052
+ * `exitCode` is null because the gate's step result does not carry one:
1053
+ * `CommandResult` reports `ok`, not a code. The field stays in
1054
+ * `FailureFacts` because it is part of what identifies a failure where a
1055
+ * caller does have it, and null is honest about not having it here.
1056
+ */
1057
+ this.lastFailure = {
1058
+ step: failed.step,
1059
+ exitCode: null,
1060
+ outputDigest: failed.outputDigest ?? null,
1061
+ };
1062
+ }
1063
+ const why = gate.verdict === 'abort'
1064
+ ? `gate abort: ${gate.abort?.reason ?? 'interlock tripped'}`
1065
+ : `gate red: step ${failedStep ?? '(unknown)'} failed`;
1066
+ state = (0, core_1.transition)(state, (0, core_1.afterGate)(gate.verdict));
1067
+ const outcome = await this.discardAndRequeue(round, state, item.id, attempt, why, result.testsAdded, {
1068
+ stage: 'gate',
1069
+ gate,
1070
+ diagnostics,
1071
+ implementer: impl,
1072
+ });
1073
+ return { outcome, title: item.title };
1074
+ }
1075
+ // The gate-side half of the commit permit, checked before proof so a round
1076
+ // that can never commit does not pay for a prover call. `proofEnabled: false`
1077
+ // because the proof key is turned below via the state machine — a `partial`
1078
+ // answered "keep" is a human turning it, which a strict verdict check here
1079
+ // would wrongly veto. The load-bearing case: a *required* step skipped by its
1080
+ // `when:` filter leaves the gate green and must still block the commit —
1081
+ // tabsy stamped 166 trailers with a check that never once ran.
1082
+ const permit = (0, core_1.mayCommit)({
1083
+ steps: gate.steps,
1084
+ requiredSteps: (0, gate_1.requiredSteps)(this.cfg.gates),
1085
+ proofEnabled: false,
1086
+ });
1087
+ if (!permit.ok) {
1088
+ state = (0, core_1.transition)(state, 'discarded');
1089
+ const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `commit refused: ${permit.reason ?? 'unknown'}`, result.testsAdded, { stage: 'commit', gate, diagnostics, implementer: impl });
1090
+ return { outcome, title: item.title };
1091
+ }
1092
+ state = (0, core_1.transition)(state, 'proving');
1093
+ await this.emit('round.phase', { round, phase: 'proving' });
1094
+ let proof;
1095
+ if (this.cfg.proof.enabled && this.deps.prove) {
1096
+ try {
1097
+ proof = await this.deps.prove({ item, changedPaths: changed });
1098
+ }
1099
+ catch (err) {
1100
+ // §12 case 15's harder sibling: an unparseable verdict is a rejection,
1101
+ // and a prover that *crashes* (spawn failure, transport down) must be
1102
+ // no kinder — this throw used to escape runRound and kill the whole
1103
+ // overnight run. Without the second key the round may never commit, so
1104
+ // the tree is discarded and the item requeued unproven: one lost
1105
+ // round, not the night. The alert is critical because a down prover
1106
+ // discards every round until someone hears.
1107
+ const why = err instanceof Error ? err.message : String(err);
1108
+ await this.emit('alert', {
1109
+ code: 'prover-error',
1110
+ severity: 'critical',
1111
+ text: `prover invocation failed: ${why}`,
1112
+ });
1113
+ /**
1114
+ * ⚠️ THE VERIFIER, NOT THE GATE. Recorded as its own kind because "a
1115
+ * deterministic gate refused it" is a different fact about a different
1116
+ * mechanism, and a later reader deciding what keeps going wrong was
1117
+ * being handed a category that had been flattened.
1118
+ */
1119
+ this.failedAtThisRound = 'verifier';
1120
+ state = (0, core_1.transition)(state, 'discarded');
1121
+ const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `prover error: ${why}`, result.testsAdded, { stage: 'proof', gate, diagnostics, implementer: impl });
1122
+ return { outcome, title: item.title };
1123
+ }
1124
+ await this.emit('proof.result', { round, verdict: proof.verdict, reason: proof.reason, confidence: proof.confidence }, 'prover');
1125
+ const resolution = (0, core_1.afterProof)(proof.verdict);
1126
+ let final;
1127
+ /** M6-20: set when the policy was `ask`, nothing could ask, and the answer was therefore `discard`. */
1128
+ let askedAndDiscarded = null;
1129
+ if (resolution.kind === 'decided') {
1130
+ final = resolution.state;
1131
+ }
1132
+ else {
1133
+ // `partial`: the round stays in `proving` until an answer exists.
1134
+ // on_partial keep/discard IS the answer; 'ask' delegates to whoever can
1135
+ // reach a human. The 'discard' fallback is unreachable (constructor
1136
+ // refuses ask-with-nothing-wired) but keeps the safe default explicit.
1137
+ const ask = this.deps.resolvePartial;
1138
+ const answer = this.cfg.proof.onPartial !== 'ask' ? this.cfg.proof.onPartial : ask ? await ask(item, proof) : 'discard';
1139
+ /**
1140
+ * M6-20: A DISCARD THE PROJECT DID NOT CHOOSE IS SAID OUT LOUD.
1141
+ *
1142
+ * Best-effort, like every other alert here: a ledger that refuses it
1143
+ * must not change what happens to the round. Written before the
1144
+ * outcome is applied so it sits with the round it belongs to.
1145
+ */
1146
+ if (this.cfg.proof.onPartial === 'ask' && answer === 'discard') {
1147
+ askedAndDiscarded = partialDiscardedText(item.title, proof.reason);
1148
+ try {
1149
+ await this.emit('alert', {
1150
+ code: exports.PARTIAL_DISCARDED_CODE,
1151
+ severity: 'warn',
1152
+ text: askedAndDiscarded,
1153
+ });
1154
+ }
1155
+ catch {
1156
+ /* the round's own record below is the one that must land */
1157
+ }
1158
+ }
1159
+ final = (0, core_1.afterAnswer)(answer);
1160
+ }
1161
+ if (final !== 'committed') {
1162
+ /**
1163
+ * 🔴 THE VERIFIER REFUSED, AND THIS RECORDED IT AS A GATE FAILURE — the
1164
+ * exact case the producer's own docblock says it fixed.
1165
+ *
1166
+ * Only a prover CRASH ever set `'verifier'`. A prover that ran fine and
1167
+ * said `refuted` left `failedAtThisRound` at null, and null maps to
1168
+ * `gate_failed` — so the record asserted that a deterministic gate
1169
+ * refused the round, in a record that simultaneously said both gate
1170
+ * steps PASSED. Two contradictory statements in one row, and the
1171
+ * loop-safety detectors and the person both read the wrong one.
1172
+ *
1173
+ * A refusal is the prover doing its job, which is what makes recording
1174
+ * it as a gate failure so misleading: it is the single most important
1175
+ * signal that the model and the checker disagree, and it was filed
1176
+ * under the mechanism that had nothing to say.
1177
+ */
1178
+ this.failedAtThisRound = 'verifier';
1179
+ state = (0, core_1.transition)(state, final);
1180
+ const outcome = await this.discardAndRequeue(round, state, item.id, attempt,
1181
+ // M6-20: the same sentence the alert carries, so the ITEM says why
1182
+ // too — "proof partial: …" alone reads as the checker refusing.
1183
+ askedAndDiscarded ?? `proof ${proof.verdict}: ${proof.reason}`, result.testsAdded, { stage: 'proof', gate, diagnostics, proof, implementer: impl });
1184
+ return { outcome, title: item.title };
1185
+ }
1186
+ }
1187
+ /**
1188
+ * 🔴 THE LAST LOOK, BECAUSE EVERY CHECK ABOVE RAN ON A PRE-GATE SNAPSHOT
1189
+ * AND THE NEXT LINE IS `git add -A`.
1190
+ *
1191
+ * `changed` was read once, before the gate. The gate then runs the
1192
+ * PROJECT'S OWN COMMANDS — a build, a formatter, a codegen step, a test
1193
+ * suite that writes fixtures — and the prover runs after it. Anything any
1194
+ * of them wrote lands in the working tree, is staged by `stageAll`, and is
1195
+ * committed without ever having been shown to the rails interlock or the
1196
+ * surface check. Forbidden paths, production env files, CI workflow files:
1197
+ * all of them ride in through a window the checks were never told about.
1198
+ *
1199
+ * So the tree is read AGAIN here, and the two checks that guard a commit
1200
+ * are re-asked on what is actually about to be committed.
1201
+ *
1202
+ * ⚠️ AND THE FENCE IS ASKED HERE, WHICH IS THE ONLY PLACE IT MEANS
1203
+ * ANYTHING. `surface-lease.ts` says "EXPIRY EXISTS ONLY HERE, AND ONLY
1204
+ * BECAUSE OF THE FENCE" — and `checkFence` had NO production caller at all,
1205
+ * so the expiry shipped and the thing that made expiry safe did not. A
1206
+ * holder whose lease lapsed while a newer run took the surface could commit
1207
+ * with no comparison anywhere in the path. Two integers, at the moment of
1208
+ * the write, exactly as the design said.
1209
+ */
1210
+ const atCommit = await this.deps.git.changedPaths();
1211
+ const lateTrespass = (0, core_1.surfaceViolations)(atCommit, this.deps.declaredSurfaces ?? []);
1212
+ if (lateTrespass.length > 0) {
1213
+ const paths = lateTrespass.map((v) => v.path);
1214
+ this.surfaceViolation = { paths, declared: this.deps.declaredSurfaces ?? [] };
1215
+ state = (0, core_1.transition)(state, 'discarded');
1216
+ const outcome = await this.discardAndReturn(round, state, item.id, `the gate or the prover wrote outside the surfaces this run declared ` +
1217
+ `(${(this.deps.declaredSurfaces ?? []).join(', ')}): ` +
1218
+ `${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''}`, false);
1219
+ return { outcome, title: item.title };
1220
+ }
1221
+ const fenceTrouble = this.deps.checkFence?.();
1222
+ if (fenceTrouble !== undefined && !fenceTrouble.ok) {
1223
+ state = (0, core_1.transition)(state, 'discarded');
1224
+ const outcome = await this.discardAndReturn(round, state, item.id, fenceTrouble.because, false);
1225
+ return { outcome, title: item.title };
1226
+ }
1227
+ const lateRails = (0, gate_1.checkInterlocks)({
1228
+ changedPaths: atCommit,
1229
+ rails: this.cfg.rails,
1230
+ // The environment was judged before the gate and cannot have changed by
1231
+ // the gate writing files. Only the DIFF is re-asked here.
1232
+ envValues: [],
1233
+ itemId: item.id,
1234
+ now: this.deps.now(),
1235
+ exemptions: this.deps.exemptionsFor?.(item.id) ?? [],
1236
+ });
1237
+ if (lateRails !== null) {
1238
+ state = (0, core_1.transition)(state, 'discarded');
1239
+ const outcome = await this.discardAndReturn(round, state, item.id, `the gate or the prover wrote somewhere the rails forbid: ${lateRails.reason}` +
1240
+ (lateRails.paths?.length ? ` (${lateRails.paths.slice(0, 3).join(', ')})` : ''), false);
1241
+ return { outcome, title: item.title };
1242
+ }
1243
+ /**
1244
+ * 🔴 THE CROSS-REPO NEEDS ARE ASKED AGAIN HERE, AND THIS IS THE SECOND OF
1245
+ * THE TWO PLACES THE CHARTER NAMES.
1246
+ *
1247
+ * `OWNER_CHARTER.md` §6.D: *"Enforce at pick/admission where the decision is
1248
+ * used; check again at relevant acceptance boundaries if dependencies
1249
+ * changed."* The pick is the first; this is the second, and it exists
1250
+ * because a round is long. The item was pickable when it was picked; by the
1251
+ * time its work is about to be kept, the accepted set can have drifted, a
1252
+ * repository can have moved off the revision this item was written against,
1253
+ * or the set can have been replaced entirely. Keeping the work anyway would
1254
+ * commit code written against a state that no longer exists — which is the
1255
+ * same failure `checkFence` and the late rails guard, arriving from a
1256
+ * different direction.
1257
+ *
1258
+ * ⚠️ IT FAILS CLOSED. An item that declares needs and whose context cannot
1259
+ * be read now reaches `undeclared` and is discarded, exactly as it would at
1260
+ * the pick. "Could not establish" is not "still fine".
1261
+ *
1262
+ * ⚠️ AND THE EVIDENCE IS KEPT (`false`). Nothing about the item's own claim
1263
+ * was disproved — a sibling moved. Invalidating would route the item back
1264
+ * through the verifier for a reason that has nothing to do with it, which is
1265
+ * the mistake `discardAndReturn`'s own docblock records about environment
1266
+ * blocks.
1267
+ */
1268
+ if ((item.needs?.length ?? 0) > 0) {
1269
+ const dependenciesNow = (await this.deps.crossRepo?.()) ?? { declared: [], set: null, current: new Map() };
1270
+ const moved = (0, core_1.itemCrossRepoBlockedBy)(item, dependenciesNow);
1271
+ if (moved.length > 0) {
1272
+ state = (0, core_1.transition)(state, 'discarded');
1273
+ const outcome = await this.discardAndReturn(round, state, item.id, `a repository this item depends on moved while the round was running: ${(0, core_1.describeItemCrossRepoBlockers)(moved)}`, false);
1274
+ return { outcome, title: item.title };
1275
+ }
1276
+ }
1277
+ await this.deps.git.stageAll();
1278
+ const trailer = { steps: gate.steps };
1279
+ if (proof)
1280
+ trailer.proof = proof;
1281
+ if (gate.exemptionsUsed?.length) {
1282
+ // The grant appears on the commit itself, so a reader of the log can see
1283
+ // the door was opened — a silently-exempted rail defeats the audit (§8).
1284
+ trailer.exemptions = gate.exemptionsUsed.map((e) => ({ path: e.path, actor: e.actor }));
1285
+ }
1286
+ /**
1287
+ * 🔴 ALREADY DONE IS NOT AN ERROR, AND IT USED TO END THE RUN.
1288
+ *
1289
+ * An item whose behaviour a previous round already delivered leaves nothing
1290
+ * staged. `git commit` then exits 1, which becomes a `GitError`, which the
1291
+ * crash path turns into `run-aborted` — critical, run over, every remaining
1292
+ * item dropped. Measured live: gate green in 2,148 ms, prover `proven` at
1293
+ * 0.83, run killed.
1294
+ *
1295
+ * The honest reading is the one the evidence supports. The gate passed and
1296
+ * the prover affirmed this item's claim against the tree, so the behaviour
1297
+ * IS present — it simply arrived in an earlier commit. So the item is
1298
+ * marked `done` on that evidence, and the round ends without inventing a
1299
+ * commit that does not exist.
1300
+ *
1301
+ * ⚠️ `discarded`, NOT `committed`, because no new commit was made and a sha
1302
+ * would have to be borrowed from a round that already reported it. The item
1303
+ * is `done`, so `shouldRequeue` cannot spin it: a done item is not pickable.
1304
+ */
1305
+ if (this.deps.git.hasStagedChanges !== undefined && !(await this.deps.git.hasStagedChanges())) {
1306
+ this.roundNote =
1307
+ `nothing left to commit — the gate and the prover both passed, so this item's work is ` +
1308
+ `already in the tree from an earlier round. Marked done rather than ending the run.`;
1309
+ state = (0, core_1.transition)(state, 'discarded');
1310
+ const outcome = await this.endRound(round, state, item.id, { state: 'done' }, { files: 0 });
1311
+ return { outcome, title: item.title };
1312
+ }
1313
+ const sha = await this.deps.git.commit(item.title, trailer);
1314
+ // Spend on commit, not on check: a discarded round left nothing behind, and
1315
+ // burning the grant on a failure would strand the retry outside its own
1316
+ // door. Spending happens after the commit exists, so the round the copy
1317
+ // names is a round that really landed; a spend-persist failure throws — a
1318
+ // grant we cannot record as spent would silently become a many-round door.
1319
+ for (const e of gate.exemptionsUsed ?? []) {
1320
+ await this.deps.spendExemption?.((0, core_1.markExemptionUsed)(e, round));
1321
+ }
1322
+ state = (0, core_1.transition)(state, 'committed');
1323
+ const outcome = await this.endRound(round, state, item.id, { state: 'done' }, { sha, files: changed.length, testsAdded: result.testsAdded });
1324
+ return { outcome, title: item.title };
1325
+ }
1326
+ /**
1327
+ * Discard the tree, and with it the item's cached evidence — §12 case 19's
1328
+ * "always after a discard". Unconditional across every discard path: a
1329
+ * requeued or later-unblocked item will be picked again, and its next attempt
1330
+ * must not start from the very file:line claims that just led into a wall.
1331
+ * (For an item closed by the same call the entry is merely dead weight.)
1332
+ */
1333
+ async discardTree(itemId, invalidateEvidence = true) {
1334
+ await this.deps.git.discard();
1335
+ if (invalidateEvidence)
1336
+ await this.deps.evidence?.invalidate(itemId);
1337
+ }
1338
+ /**
1339
+ * Discard the tree and requeue: back to `ready` with attempt++ and the reason
1340
+ * carried, so the next attempt starts knowing why this one failed (§2.2).
1341
+ */
1342
+ async discardAndRequeue(round, state, itemId, attempt, why, testsAdded, context = { stage: 'implementer' }) {
1343
+ /**
1344
+ * 🔴 RETAINED BEFORE THE RESET, NOT AFTER. `discardTree` is `reset --hard`
1345
+ * and the only copy of the candidate is the working tree. Every path that
1346
+ * spends an attempt comes through here, so every rejected candidate is
1347
+ * kept — or its absence is recorded on the round, by name.
1348
+ */
1349
+ const kept = await this.retainRejected(round, itemId, attempt, why, context);
1350
+ await this.discardTree(itemId);
1351
+ return this.endRound(round, state, itemId,
1352
+ // Back to the queue, unless that was the last attempt this item gets.
1353
+ // `afterSpentAttempt` owns that decision so this and recovery — the only
1354
+ // two places an attempt is ever spent — cannot come to disagree about it.
1355
+ { state: (0, core_1.afterSpentAttempt)(attempt), attempts: attempt, lastFailure: why }, { ...(testsAdded === undefined ? {} : { testsAdded }), ...kept });
1356
+ }
1357
+ /**
1358
+ * Write the candidate and its verdict where the reset cannot reach, and
1359
+ * answer the fields `round.ended` carries about it.
1360
+ *
1361
+ * NEVER THROWS. A failure to keep evidence is a fact about the round
1362
+ * (`evidenceIncomplete`), not a reason to leave the tree dirty — the discard
1363
+ * that follows this must run whatever happened here.
1364
+ */
1365
+ async retainRejected(round, itemId, attempt, why, context) {
1366
+ const failedStep = context.gate?.steps.find((s) => !s.skipped && !s.ok)?.step;
1367
+ const named = failedStep === undefined ? {} : { failedStep };
1368
+ const sink = this.deps.rejectedEvidence;
1369
+ if (sink === undefined) {
1370
+ return { ...named, evidenceIncomplete: 'no evidence store is wired for this run' };
1371
+ }
1372
+ let input;
1373
+ try {
1374
+ input = await this.rejectionInput(round, itemId, attempt, why, context);
1375
+ }
1376
+ catch (e) {
1377
+ return { ...named, evidenceIncomplete: `evidence could not be gathered: ${e.message}` };
1378
+ }
1379
+ let outcome;
1380
+ try {
1381
+ outcome = await sink.retain(input);
1382
+ }
1383
+ catch (e) {
1384
+ outcome = { retained: false, because: `evidence store threw: ${e.message}` };
1385
+ }
1386
+ this.rejections.set(itemId, { input, outcome });
1387
+ return outcome.retained
1388
+ ? { ...named, evidencePath: outcome.path }
1389
+ : { ...named, evidenceIncomplete: outcome.because };
1390
+ }
1391
+ async rejectionInput(round, itemId, attempt, why, context) {
1392
+ const git = this.deps.git;
1393
+ const item = this.deps.queue.get(itemId);
1394
+ const manifest = git.changedEntries !== undefined
1395
+ ? await git.changedEntries()
1396
+ : (await git.changedPaths()).map((path) => ({ path, status: '??' }));
1397
+ /**
1398
+ * ⚠️ AN ABORTED CANDIDATE'S PATCH IS WITHHELD. The rails fired because the
1399
+ * change touched a path the project forbids; its content is precisely what
1400
+ * must not be copied into a record. The manifest still names the paths.
1401
+ */
1402
+ const aborted = context.gate?.verdict === 'abort';
1403
+ let patch = null;
1404
+ let patchWithheld;
1405
+ if (aborted) {
1406
+ patchWithheld =
1407
+ 'the rails aborted this candidate: it touched a forbidden path, and the content of ' +
1408
+ 'such a change is not retained';
1409
+ }
1410
+ else if (git.patch === undefined) {
1411
+ patchWithheld = 'this git driver cannot produce a patch';
1412
+ }
1413
+ else {
1414
+ patch = await git.patch(rejected_evidence_1.REJECTED_PATCH_MAX_BYTES);
1415
+ }
1416
+ const impl = context.implementer;
1417
+ return {
1418
+ run: { runId: this.id.runId, projectId: this.id.projectId, lane: this.id.lane, branch: this.id.branch },
1419
+ round,
1420
+ attempt,
1421
+ itemId,
1422
+ itemTitle: item?.title ?? '',
1423
+ baseSha: await git.headSha(),
1424
+ why,
1425
+ stage: context.stage,
1426
+ ...(context.gate === undefined
1427
+ ? {}
1428
+ : {
1429
+ gate: {
1430
+ verdict: context.gate.verdict,
1431
+ steps: context.gate.steps,
1432
+ diagnostics: context.diagnostics ?? [],
1433
+ ...(context.gate.abort === undefined ? {} : { abort: context.gate.abort }),
1434
+ },
1435
+ }),
1436
+ ...(context.proof === undefined ? {} : { proof: context.proof }),
1437
+ ...(impl === undefined
1438
+ ? {}
1439
+ : {
1440
+ implementer: {
1441
+ result: impl.kind === 'result' ? impl.result : null,
1442
+ abandoned: impl.kind === 'abandoned' ? impl.reason : null,
1443
+ transcript: impl.transcript,
1444
+ },
1445
+ }),
1446
+ manifest,
1447
+ patch,
1448
+ ...(patchWithheld === undefined ? {} : { patchWithheld }),
1449
+ };
1450
+ }
1451
+ /**
1452
+ * What this attempt is told about the last one, and whether that candidate
1453
+ * was put back first.
1454
+ *
1455
+ * RESTORED ONLY WHEN ALL OF THESE HOLD, and each refusal names itself:
1456
+ * - the driver can apply a patch and one was retained, whole
1457
+ * - HEAD is the base the candidate was written against
1458
+ * - every path it touched is inside the project's declared scope and
1459
+ * outside its rails — the candidate is re-gated afterwards regardless,
1460
+ * but a restore must not put a change somewhere the worker itself may
1461
+ * not write
1462
+ * A restore that fails leaves the tree as it was (`applyPatch` is
1463
+ * all-or-nothing, and the tree is discarded again to be sure) and the
1464
+ * worker is told it starts from the base, and why.
1465
+ */
1466
+ async prepareRepair(item, attempt) {
1467
+ let last = this.rejections.get(item.id);
1468
+ let recoveredFrom;
1469
+ if (last === undefined) {
1470
+ // A restart between the rejection and this attempt: the memory is gone,
1471
+ // the retained evidence is not. See `recoverRejection`.
1472
+ const recovered = await this.recoverRejection(item);
1473
+ if (recovered === undefined)
1474
+ return undefined;
1475
+ last = recovered.last;
1476
+ recoveredFrom = recovered.from;
1477
+ }
1478
+ const { input, outcome } = last;
1479
+ const previous = {
1480
+ round: input.round,
1481
+ attempt: input.attempt,
1482
+ why: input.why,
1483
+ stage: input.stage,
1484
+ failedStep: input.gate?.steps.find((s) => !s.skipped && !s.ok)?.step ?? null,
1485
+ evidencePath: outcome.retained ? outcome.path : null,
1486
+ diagnostics: input.gate?.diagnostics ?? [],
1487
+ manifest: input.manifest,
1488
+ patch: input.patch === null ? null : { text: input.patch.text, truncated: input.patch.truncated },
1489
+ patchWithheld: input.patchWithheld ?? null,
1490
+ ...(recoveredFrom === undefined ? {} : { recoveredFrom }),
1491
+ };
1492
+ const base = { attempt, maxAttempts: core_1.MAX_ITEM_EXECUTION_ATTEMPTS, previous };
1493
+ const refuse = (because) => this.noteRepair({ ...base, restored: { ok: false, because } });
1494
+ const git = this.deps.git;
1495
+ if (git.applyPatch === undefined)
1496
+ return refuse('this git driver cannot restore a patch');
1497
+ if (!outcome.retained || outcome.patchFile === null) {
1498
+ return refuse(input.patchWithheld ?? 'the previous candidate was not retained');
1499
+ }
1500
+ if (input.patch === null || input.patch.truncated) {
1501
+ return refuse('the retained patch is truncated and cannot be applied whole');
1502
+ }
1503
+ const head = await git.headSha();
1504
+ if (head !== input.baseSha) {
1505
+ return refuse(`the tree has moved on: the candidate was written against ${input.baseSha}, HEAD is ${head}`);
1506
+ }
1507
+ const paths = input.manifest.map((m) => m.path);
1508
+ /**
1509
+ * 🔴 THIS USED TO PERMIT `[...scope.free, ...scope.additive]`, WHICH IS
1510
+ * THE ONE THING `additive` PROMISES NOT TO MEAN. Restoring a candidate
1511
+ * writes its patch back into the tree, so a manifest that MODIFIED a file
1512
+ * under an additive-only grant was put back by the check that existed to
1513
+ * stop it — and this was the only place in the product where scope was
1514
+ * compared against anything at all.
1515
+ *
1516
+ * It now asks the same fold the gate interlock asks, so a restore cannot
1517
+ * be more permissive than the gate that will re-judge it.
1518
+ */
1519
+ const restoreEntries = input.manifest.map((m) => ({ path: m.path, status: m.status }));
1520
+ const scopeFails = (0, scope_rule_1.scopeViolations)(restoreEntries, this.cfg.scope);
1521
+ if (scopeFails.length > 0) {
1522
+ const named = scopeFails.map((v) => `${v.path} (${v.because})`).join(', ');
1523
+ return refuse(`the candidate touched paths outside the declared scope: ${named}`);
1524
+ }
1525
+ const railed = (0, glob_1.filterMatching)(paths, this.cfg.rails.forbiddenPaths);
1526
+ if (railed.length > 0)
1527
+ return refuse(`the candidate touched railed paths: ${railed.join(', ')}`);
1528
+ const applied = await git.applyPatch(outcome.patchFile);
1529
+ if (!applied.ok) {
1530
+ await git.discard();
1531
+ return refuse(applied.because);
1532
+ }
1533
+ return this.noteRepair({ ...base, restored: { ok: true, paths } });
1534
+ }
1535
+ /** Records what this round’s repair was, for `round.ended`, and hands the context back unchanged. */
1536
+ noteRepair(ctx) {
1537
+ this.roundRepair = {
1538
+ previousAttempt: ctx.previous.attempt,
1539
+ restored: ctx.restored.ok,
1540
+ ...(ctx.restored.ok ? { restoredPaths: [...ctx.restored.paths] } : { notRestored: ctx.restored.because }),
1541
+ ...(ctx.previous.recoveredFrom === undefined ? {} : { recoveredFrom: ctx.previous.recoveredFrom }),
1542
+ };
1543
+ return ctx;
1544
+ }
1545
+ /**
1546
+ * The item’s last rejection from an EARLIER run, when the evidence store can
1547
+ * vouch for it — see `RejectedEvidenceSink.latestFor` for every check.
1548
+ *
1549
+ * ⚠️ A REFUSAL IS RECORDED, NOT SILENT. The attempt then goes ahead briefed
1550
+ * from `lastFailure` and starting from the base, exactly as before recovery
1551
+ * existed — but `round.ended` says a restart found evidence and why it was
1552
+ * not used, so "no repair context" can be told apart from "refused one".
1553
+ */
1554
+ async recoverRejection(item) {
1555
+ const sink = this.deps.rejectedEvidence;
1556
+ if (sink?.latestFor === undefined)
1557
+ return undefined;
1558
+ const found = await sink.latestFor({
1559
+ itemId: item.id,
1560
+ projectId: this.id.projectId,
1561
+ lane: this.id.lane,
1562
+ runId: this.id.runId,
1563
+ attemptsSpent: item.attempts,
1564
+ lastFailure: item.lastFailure,
1565
+ });
1566
+ if (!found.found) {
1567
+ if (found.because !== null) {
1568
+ this.roundRepair = { previousAttempt: null, restored: false, recoveryRefused: found.because };
1569
+ }
1570
+ return undefined;
1571
+ }
1572
+ return {
1573
+ last: { input: found.input, outcome: found.outcome },
1574
+ from: { runId: found.input.run.runId, evidencePath: found.outcome.path },
1575
+ };
1576
+ }
1577
+ /**
1578
+ * Discard the tree and put the item back exactly as it was found.
1579
+ *
1580
+ * THE ATTEMPT IS NOT SPENT, and that is the whole difference from
1581
+ * `discardAndRequeue`. An attempt is the budget for an item's implementation
1582
+ * FAILING, and nothing failed here — the loop was stopped from outside before
1583
+ * this round could reach a verdict. Spending one anyway would mean an item
1584
+ * that keeps meeting the same external boundary is `closed` by
1585
+ * `afterSpentAttempt` after three interruptions, without a single check ever
1586
+ * having gone red, and while whatever the run was waiting for may since have
1587
+ * been answered.
1588
+ *
1589
+ * The tree is still discarded, because every path in this file discards
1590
+ * un-gated work: `run()` refuses to start on a dirty tree, and leftovers
1591
+ * would fold into the next round's diff.
1592
+ *
1593
+ * The reason still travels on the item, which is what a person or a later
1594
+ * attempt reads to find out why this round produced nothing.
1595
+ */
1596
+ async discardAndReturn(round, state, itemId, why,
1597
+ /**
1598
+ * Whether to drop the item's cached evidence along with the tree.
1599
+ *
1600
+ * ⚠️ FALSE FOR AN ENVIRONMENT BLOCK, and that is not an optimisation.
1601
+ * §12 case 19's rationale for invalidating is "its next attempt must not
1602
+ * start from the very file:line claims that just led into a wall" — and
1603
+ * nothing ran, so no wall was met. Invalidating anyway makes the item a
1604
+ * guaranteed cache miss, and `pickNext` then routes it through the VERIFIER
1605
+ * before the next round: a paid model call, on the machine the preflight
1606
+ * just declared unusable. Measured at one verifier turn per blocked round.
1607
+ */
1608
+ invalidateEvidence = true) {
1609
+ await this.discardTree(itemId, invalidateEvidence);
1610
+ return this.endRound(round, state, itemId, { state: 'ready', lastFailure: why });
1611
+ }
1612
+ async endRound(round, state, itemId, patch, extra = {}) {
1613
+ const outcome = (0, core_1.outcomeOf)(state);
1614
+ if (outcome === null)
1615
+ throw new RunnerError(`round ended in non-terminal state ${state}`);
1616
+ await this.patchItem(itemId, patch);
1617
+ // The note goes on before `extra`, so a caller that has something better to
1618
+ // say about this particular ending can still say it.
1619
+ await this.emit('round.ended', {
1620
+ round,
1621
+ outcome,
1622
+ itemId,
1623
+ ...(this.roundNote === undefined ? {} : { note: this.roundNote }),
1624
+ ...(this.roundRepair === undefined ? {} : { repair: this.roundRepair }),
1625
+ ...extra,
1626
+ });
1627
+ this.totals[outcome] += 1;
1628
+ /**
1629
+ * WHERE THIS RED CAME FROM, counted — see {@link RunTotals.proverRejected}.
1630
+ *
1631
+ * `failedAtThisRound` is already maintained on every round regardless of
1632
+ * `run.per_round_safety`, so this costs nothing and needs no flag. It is
1633
+ * set to `'verifier'` exactly where the prover refutes or returns an
1634
+ * unusable verdict, and to `'gate'` where a check went red — which is the
1635
+ * distinction the summary was inventing.
1636
+ */
1637
+ if (outcome === 'discarded' && this.failedAtThisRound === 'verifier') {
1638
+ this.totals.proverRejected = (this.totals.proverRejected ?? 0) + 1;
1639
+ }
1640
+ return outcome;
1641
+ }
1642
+ /**
1643
+ * Queue writes ride the store's optimistic concurrency. When the phone edited
1644
+ * the item mid-round the runner loses the version race — its write records
1645
+ * what physically happened to the round, so it refetches and lands on top of
1646
+ * the curation edit rather than crashing an overnight run over the race.
1647
+ */
1648
+ async patchItem(id, patch) {
1649
+ const current = this.deps.queue.get(id);
1650
+ if (current === undefined)
1651
+ throw new RunnerError(`item ${id} vanished from the queue mid-round`);
1652
+ const first = await this.deps.queue.update(id, current.version, patch);
1653
+ if (first.ok)
1654
+ return first.item;
1655
+ if (first.reason === 'stale-version') {
1656
+ const again = this.deps.queue.get(id);
1657
+ if (again !== undefined) {
1658
+ const second = await this.deps.queue.update(id, again.version, patch);
1659
+ if (second.ok)
1660
+ return second.item;
1661
+ }
1662
+ }
1663
+ throw new RunnerError(`queue write failed for ${id}: ${first.reason}`);
1664
+ }
1665
+ /** Same inputs `doctor` probes: prod env files that exist, plus the live env. */
1666
+ collectEnvValues() {
1667
+ const values = [];
1668
+ for (const file of this.cfg.rails.prodEnvFiles) {
1669
+ const text = this.deps.readEnvFile(file);
1670
+ if (text !== null)
1671
+ values.push(text);
1672
+ }
1673
+ for (const v of Object.values(this.deps.env)) {
1674
+ if (v)
1675
+ values.push(v);
1676
+ }
1677
+ return values;
1678
+ }
1679
+ async emit(kind, data, actor = 'runner') {
1680
+ const input = {
1681
+ ts: new Date(this.deps.now()).toISOString(),
1682
+ runId: this.id.runId,
1683
+ projectId: this.id.projectId,
1684
+ lane: this.id.lane,
1685
+ kind,
1686
+ actor,
1687
+ data,
1688
+ };
1689
+ await this.deps.ledger.append(input);
1690
+ }
1691
+ }
1692
+ exports.RoundRunner = RoundRunner;
1693
+ //# sourceMappingURL=round-runner.js.map