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,4418 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REPAIR_PROMPT_PATCH_MAX_CHARS = exports.REPAIR_PROMPT_OUTPUT_MAX_CHARS = exports.EXIT_ATTEMPT_CLAIMED = exports.EXIT_REFUSED = exports.RunRefused = void 0;
4
+ exports.writeEndingBeforeFirstRound = writeEndingBeforeFirstRound;
5
+ exports.describeRepair = describeRepair;
6
+ exports.buildImplementerPrompt = buildImplementerPrompt;
7
+ exports.runLoop = runLoop;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ const workspace_1 = require("./workspace");
13
+ const trusted_git_1 = require("./trusted-git");
14
+ const core_1 = require("@looply/core");
15
+ const config_1 = require("./config");
16
+ const ledger_store_1 = require("./ledger-store");
17
+ const run_ended_1 = require("./run-ended");
18
+ const exemption_store_1 = require("./exemption-store");
19
+ const queue_store_1 = require("./queue-store");
20
+ const lane_claims_1 = require("./lane-claims");
21
+ const run_delivery_1 = require("./run-delivery");
22
+ const goal_met_target_1 = require("./goal-met-target");
23
+ const reaper_1 = require("./reaper");
24
+ const requirement_claims_1 = require("./requirement-claims");
25
+ const evidence_cache_1 = require("./evidence-cache");
26
+ const control_file_1 = require("./control-file");
27
+ const git_driver_1 = require("./git-driver");
28
+ const untrusted_gate_1 = require("./untrusted-gate");
29
+ const model_transport_1 = require("./model-transport");
30
+ const agent_envelope_1 = require("./agent-envelope");
31
+ const provider_limit_1 = require("./provider-limit");
32
+ const rejected_evidence_1 = require("./rejected-evidence");
33
+ const owned_children_1 = require("./owned-children");
34
+ const verify_cmd_1 = require("./verify-cmd");
35
+ const verify_pass_1 = require("./verify-pass");
36
+ const verify_cmd_2 = require("./verify-cmd");
37
+ const gate_1 = require("./gate");
38
+ const agent_driver_1 = require("./agent-driver");
39
+ const prover_1 = require("./prover");
40
+ const notify_channel_1 = require("./notify-channel");
41
+ const notify_cadence_1 = require("./notify-cadence");
42
+ const progress_view_1 = require("./progress-view");
43
+ const signals_1 = require("./signals");
44
+ const round_runner_1 = require("./round-runner");
45
+ const syncing_ledger_1 = require("./syncing-ledger");
46
+ const sync_1 = require("./sync");
47
+ const settings_overlay_1 = require("./settings-overlay");
48
+ const queue_summary_1 = require("./queue-summary");
49
+ const run_lease_1 = require("./run-lease");
50
+ const recovery_1 = require("./recovery");
51
+ const run_start_marks_1 = require("./run-start-marks");
52
+ const start_attempts_1 = require("./start-attempts");
53
+ const recovery_io_1 = require("./recovery-io");
54
+ const daemon_cmd_1 = require("./daemon-cmd");
55
+ const goal_store_1 = require("./goal-store");
56
+ const machine_store_1 = require("./machine-store");
57
+ const server_client_1 = require("./server-client");
58
+ const programme_mode_1 = require("./programme-mode");
59
+ const partial_asker_1 = require("./partial-asker");
60
+ const goal_ownership_1 = require("./goal-ownership");
61
+ const goal_compile_1 = require("./goal-compile");
62
+ const goal_execution_evidence_1 = require("./goal-execution-evidence");
63
+ const round_records_1 = require("./round-records");
64
+ const goal_gap_1 = require("./goal-gap");
65
+ const goal_adaptive_1 = require("./goal-adaptive");
66
+ const pending_action_1 = require("./pending-action");
67
+ const approval_resolution_1 = require("./approval-resolution");
68
+ const approval_gate_read_1 = require("./approval-gate-read");
69
+ const approval_resumes_1 = require("./approval-resumes");
70
+ const project_memory_store_1 = require("./project-memory-store");
71
+ const project_index_store_1 = require("./project-index-store");
72
+ const project_index_builder_1 = require("./project-index-builder");
73
+ const index_for_head_1 = require("./index-for-head");
74
+ const declared_surfaces_1 = require("./declared-surfaces");
75
+ const packet_for_goal_1 = require("./packet-for-goal");
76
+ const project_index_facts_1 = require("./project-index-facts");
77
+ const project_graph_runtime_1 = require("./project-graph-runtime");
78
+ const provider_identity_store_1 = require("./provider-identity-store");
79
+ const agent_contained_1 = require("./agent-contained");
80
+ const core_2 = require("@looply/core");
81
+ const core_3 = require("@looply/core");
82
+ const citation_readers_1 = require("./citation-readers");
83
+ const compile_check_1 = require("./compile-check");
84
+ const compile_store_1 = require("./compile-store");
85
+ const provider_identity_1 = require("./provider-identity");
86
+ const agent_exec_1 = require("./agent-exec");
87
+ const providers_1 = require("./providers");
88
+ const core_4 = require("@looply/core");
89
+ const queue_write_lease_1 = require("./queue-write-lease");
90
+ const surface_lease_1 = require("./surface-lease");
91
+ const spend_ledger_1 = require("./spend-ledger");
92
+ const integration_1 = require("./integration");
93
+ /**
94
+ * How long a surface lease is good for without being renewed.
95
+ *
96
+ * ⚠️ GENEROUS, BECAUSE EXPIRY HERE IS A BACKSTOP AND NOT A CONTROL. A run
97
+ * inside a forty-five-minute model call is legitimately silent; the thing
98
+ * that actually stops a stale holder is the FENCE, checked at the write. The
99
+ * TTL exists so a surface is not held for ever by a machine that was
100
+ * unplugged, and six hours is past any run this product will start.
101
+ */
102
+ const SURFACE_TTL_MS = 6 * 60 * 60 * 1000;
103
+ const loop_safety_observer_1 = require("./loop-safety-observer");
104
+ const governed_approvals_1 = require("./governed-approvals");
105
+ const governed_broker_1 = require("./governed-broker");
106
+ const git_push_executor_1 = require("./git-push-executor");
107
+ const goal_depends_executor_1 = require("./goal-depends-executor");
108
+ const governed_run_1 = require("./governed-run");
109
+ const run_approval_1 = require("./run-approval");
110
+ const rails_stores_1 = require("./rails-stores");
111
+ /**
112
+ * A run that will not start, and that retrying cannot fix.
113
+ *
114
+ * Separate from every other failure because the daemon treats the two
115
+ * differently: an ordinary failure leaves the start-run queued to be offered
116
+ * again, and a command offered again every three seconds for an hour is noise
117
+ * rather than resilience. Carried out of the process as an exit code, since
118
+ * that is the only channel a detached child has.
119
+ */
120
+ class RunRefused extends Error {
121
+ code;
122
+ branch;
123
+ delivery;
124
+ /**
125
+ * The same refusal, for a machine.
126
+ *
127
+ * Carried BESIDE the message, never derived from it. 6A3 gave a refusal a
128
+ * sentence a person reads; a second machine deciding whether to spend
129
+ * anything more needs to tell "the goal is met" from "a person has to decide
130
+ * something", and those two sentences deliberately share their opening words.
131
+ *
132
+ * Every site that throws names its own code, because the code that took the
133
+ * branch is the only thing that knows which branch it took.
134
+ */
135
+ constructor(message, code = 'run_refused',
136
+ /**
137
+ * The run's branch, when the refusal came from a run that had one (M5-2).
138
+ * Carried so the mark can name whose branch the lane must publish before a
139
+ * met goal's grant closes; absent on every refusal made before a branch
140
+ * exists, and on every site that has nothing to say about one.
141
+ */
142
+ branch,
143
+ /**
144
+ * What key 2 confirmed, on a `goal_satisfied` refusal only (L4b): the
145
+ * delivering run, the commit, and each required step's outcome. Carried to
146
+ * the mark so the daemon can settle the pursuit with facts, not a sentence.
147
+ */
148
+ delivery) {
149
+ super(message);
150
+ this.code = code;
151
+ this.branch = branch;
152
+ this.delivery = delivery;
153
+ }
154
+ }
155
+ exports.RunRefused = RunRefused;
156
+ /** What `looply run` exits with when it refuses for good. */
157
+ exports.EXIT_REFUSED = 3;
158
+ /**
159
+ * THE ENDING OF A RUN THAT REGISTERED AND NEVER STARTED, WITH ITS REASON (M6-16).
160
+ *
161
+ * `releaseHold` below writes a bare `run.ended` for a run that took its slot
162
+ * and never reached its first round — a compile that produced nothing, a plan
163
+ * that came back empty, a refusal to start. M6-12 made the runner say why it
164
+ * halts; these endings never reach the runner, and until M6-16 their reason
165
+ * lived only in this process's log, which nobody reads: found live in M6-15,
166
+ * two machine Starts on a goal with nothing pickable each spent a minute
167
+ * compiling, ended `aborted` at zero rounds, and the server's feed showed the
168
+ * ending alone. The reason now travels the way the runner's does — one
169
+ * `alert { code: 'run-halted' }` immediately before the ending, on the same
170
+ * sink, so the phone reads "why" beside "ended" whichever half of the run
171
+ * wrote it. No reason recorded → no alert, never an invented sentence. Both
172
+ * writes are best-effort, each named in the log when it fails, and neither
173
+ * stops the hold being given back.
174
+ */
175
+ async function writeEndingBeforeFirstRound(sink, facts, log) {
176
+ const now = facts.now ?? (() => Date.now());
177
+ if (facts.because !== null) {
178
+ try {
179
+ await sink.append({
180
+ ts: new Date(now()).toISOString(),
181
+ runId: facts.runId,
182
+ projectId: facts.projectId,
183
+ lane: 'feature',
184
+ kind: 'alert',
185
+ actor: 'runner',
186
+ data: { code: round_runner_1.RUN_HALTED_CODE, severity: 'warn', text: facts.because },
187
+ });
188
+ }
189
+ catch (e) {
190
+ log(`run : could not record why this run ended before starting (${e.message})`);
191
+ }
192
+ }
193
+ try {
194
+ await sink.append({
195
+ ts: new Date(now()).toISOString(),
196
+ runId: facts.runId,
197
+ projectId: facts.projectId,
198
+ lane: 'feature',
199
+ kind: 'run.ended',
200
+ actor: 'runner',
201
+ data: {
202
+ runId: facts.runId,
203
+ elapsedMs: now() - facts.startedAtMs,
204
+ stopReason: 'aborted',
205
+ totals: { rounds: 0, committed: 0, discarded: 0, abandoned: 0 },
206
+ },
207
+ });
208
+ }
209
+ catch (e) {
210
+ log(`run : could not record that this run ended before starting (${e.message})`);
211
+ }
212
+ }
213
+ /**
214
+ * The exit code for "somebody else is already executing this exact attempt".
215
+ *
216
+ * Distinct from {@link EXIT_REFUSED} on purpose, and the distinction is
217
+ * load-bearing. Exit 3 means the RUN decided something about the goal, and a
218
+ * controller may end a person's grant on it. This means nothing about the goal
219
+ * at all — another process got to the same attempt first — and a controller
220
+ * that read the two as one answer would report a goal finished because two
221
+ * daemons happened to tick at the same moment.
222
+ */
223
+ exports.EXIT_ATTEMPT_CLAIMED = 4;
224
+ const AGENT_TIMEOUT_MS = 45 * 60 * 1000;
225
+ const PROVER_TIMEOUT_MS = 10 * 60 * 1000;
226
+ function evidenceLines(evidence) {
227
+ if (!evidence.length)
228
+ return '(no evidence recorded — find it yourself, and record what you find)';
229
+ return evidence
230
+ .map((e) => `- ${e.path}${e.line ? `:${e.line}` : ''}${e.quote ? ` — ${e.quote}` : ''}`)
231
+ .join('\n');
232
+ }
233
+ /** How much of a failing step's output the repair prompt carries inline. */
234
+ exports.REPAIR_PROMPT_OUTPUT_MAX_CHARS = 16 * 1024;
235
+ /** How much of the previous patch the repair prompt carries inline, when it was not restored. */
236
+ exports.REPAIR_PROMPT_PATCH_MAX_CHARS = 48 * 1024;
237
+ /**
238
+ * The previous attempt, as prose the worker can act on.
239
+ *
240
+ * Three things a repair needs and the one-line `lastFailure` never carried:
241
+ * WHAT refused the candidate and what it printed, WHAT the candidate changed,
242
+ * and WHETHER that change is in the tree now. The last is stated either way —
243
+ * "restored, these files" or "not restored, because …, you start from the
244
+ * base" — so the worker is never handed an unexplained clean tree.
245
+ *
246
+ * Bounded again here, tighter than the evidence on disk: the full logs live in
247
+ * the evidence directory, and a prompt is not the place for 256K of jest.
248
+ */
249
+ function describeRepair(repair) {
250
+ const p = repair.previous;
251
+ const lines = [];
252
+ lines.push(`This is attempt ${repair.attempt} of ${repair.maxAttempts} on this task. ` +
253
+ `Attempt ${p.attempt} (round ${p.round}) was rejected: ${p.why}`);
254
+ if (p.recoveredFrom !== undefined) {
255
+ lines.push(`That attempt was made by an earlier run (${p.recoveredFrom.runId}), which has since ended. ` +
256
+ 'What follows was recovered from its retained evidence and checked against this task and this ' +
257
+ 'tree before it was used.');
258
+ }
259
+ if (p.failedStep !== null)
260
+ lines.push(`The check that refused it was \`${p.failedStep}\`.`);
261
+ if (repair.restored.ok) {
262
+ lines.push('', 'The previous candidate has been RESTORED into the working tree, so the files it changed are ' +
263
+ 'exactly as that attempt left them. Read them; fix what the check refused; keep what was right. ' +
264
+ `Restored files: ${repair.restored.paths.join(', ') || '(none)'}`);
265
+ }
266
+ else {
267
+ lines.push('', `The previous candidate is NOT in the working tree — ${repair.restored.because}. ` +
268
+ 'The tree is at the base again. What follows is what that attempt changed, so you can ' +
269
+ 'rebuild it rather than start from nothing.');
270
+ }
271
+ const ran = p.diagnostics.filter((d) => d.classification !== 'passed');
272
+ for (const d of ran) {
273
+ const exit = d.classification === 'timed_out'
274
+ ? 'timed out — a resource outcome, not necessarily the code'
275
+ : d.classification === 'unavailable'
276
+ ? 'could not run — the environment, not the code'
277
+ : `exited ${d.exitCode ?? '(unknown)'}`;
278
+ const out = (0, gate_1.boundText)(d.output, exports.REPAIR_PROMPT_OUTPUT_MAX_CHARS);
279
+ lines.push('', `### Output of \`${d.step}\` (\`${d.command}\`, ${exit})`, '', '```text', out.text.trimEnd(), '```');
280
+ }
281
+ const passed = p.diagnostics.filter((d) => d.classification === 'passed').map((d) => d.step);
282
+ if (passed.length > 0)
283
+ lines.push('', `Steps that passed on that candidate: ${passed.join(', ')}.`);
284
+ if (p.manifest.length > 0) {
285
+ lines.push('', 'Files that attempt changed:', ...p.manifest.map((m) => `- ${m.path} (${m.status.trim() || '??'})`));
286
+ }
287
+ if (!repair.restored.ok) {
288
+ if (p.patch !== null) {
289
+ const patch = (0, gate_1.boundText)(p.patch.text, exports.REPAIR_PROMPT_PATCH_MAX_CHARS);
290
+ const note = p.patch.truncated || patch.truncated ? ' (truncated — the full patch is in the evidence directory)' : '';
291
+ lines.push('', `The patch that attempt produced${note}:`, '', '```diff', patch.text.trimEnd(), '```');
292
+ }
293
+ else if (p.patchWithheld !== null) {
294
+ lines.push('', `Its patch is not shown: ${p.patchWithheld}.`);
295
+ }
296
+ }
297
+ if (p.evidencePath !== null) {
298
+ lines.push('', `Full evidence for that attempt is retained at ${p.evidencePath} (not readable from inside this task).`);
299
+ }
300
+ lines.push('', 'The acceptance criteria and the scope are unchanged from the previous attempt.');
301
+ return `\n## What happened to the previous attempt\n\n${lines.join('\n')}\n`;
302
+ }
303
+ /**
304
+ * The implementer prompt.
305
+ *
306
+ * Cached evidence goes in verbatim so the agent starts at the right file and
307
+ * line instead of re-searching for a defect somebody already located — that
308
+ * saving is most of what pays for the verification pass.
309
+ *
310
+ * A previous attempt's rejection reason goes in too: an item picked a second
311
+ * time with no memory of why the first attempt was thrown away will usually
312
+ * reproduce it.
313
+ */
314
+ /**
315
+ * What this project has established, ready to embed in a prompt.
316
+ *
317
+ * 🔴 ONE PLACE DECIDES CURRENCY AND BUDGET. Two call sites need memory — the
318
+ * compiler and the implementer — and if each assembled its own, they would
319
+ * drift: one would read the tree and the other would not, and a fact stale in
320
+ * one prompt would be current in the next. The seam that produces the text is
321
+ * the seam that decides what is true enough to show.
322
+ *
323
+ * Reads the COMMITTED tree, because a fact was observed against a commit and
324
+ * `headTreeSha` is what a later run can compare it to. An unreadable tree
325
+ * yields null, which `currencyOf` treats as "cannot confirm" — so content facts
326
+ * are withheld rather than assumed, and decisions still come through.
327
+ */
328
+ /**
329
+ * Record what a PERSON decided, from the one file only a person can write.
330
+ *
331
+ * 🔴 THE AUTHORITY COMES FROM THE CHANNEL, NOT FROM A FLAG. `.looply/config.yaml`
332
+ * is in `protectedPaths()`, so the container the model runs in cannot reach it.
333
+ * That is why these may be written as `source: 'human'` — the runtime knows the
334
+ * text came from a file the model has no path to, which is a stronger statement
335
+ * than any assertion carried on the record itself.
336
+ *
337
+ * Re-read every run, because the config is the truth and a decision removed
338
+ * from it should stop being asserted. Re-recording is free: the same claim
339
+ * collapses to one record by semantic identity, so a project that never edits
340
+ * its config appends nothing after the first run.
341
+ */
342
+ async function rememberHumanDecisions(store, config, projectKey) {
343
+ if (store === null)
344
+ return;
345
+ for (const d of config.decisions) {
346
+ await store.remember({
347
+ kind: 'decision',
348
+ source: 'human',
349
+ subject: d.subject,
350
+ content: d.decision,
351
+ // The file IS the actor: it is tracked, diffable, and unreachable from
352
+ // the container. Naming a user we cannot verify would be worse.
353
+ provenance: { via: 'human-decision', refs: [], actor: 'config' },
354
+ scope: { projectKey },
355
+ }, false);
356
+ }
357
+ /**
358
+ * 🔴 AND WITHDRAW WHAT THE PERSON TOOK OUT. The comment above promised this
359
+ * and nothing implemented it: an adversarial review found that amending a
360
+ * decision left BOTH versions current for ever, so the model was handed two
361
+ * contradictory constraints with no way to tell which was live. Deleting one
362
+ * from the config did nothing at all.
363
+ *
364
+ * The config is the truth for decisions, so anything current that is no
365
+ * longer in it is withdrawn — by APPENDING a withdrawal, never by deleting.
366
+ * What a project used to hold is part of how it came to hold what it does.
367
+ */
368
+ const live = new Set(config.decisions.map((d) => `${d.subject}\u0000${d.decision}`));
369
+ for (const r of store.current(null, { kind: 'decision' })) {
370
+ if (live.has(`${r.subject}\u0000${r.content}`))
371
+ continue;
372
+ await store.remember({
373
+ kind: 'decision',
374
+ source: 'human',
375
+ subject: r.subject,
376
+ content: 'withdrawn from the project configuration',
377
+ provenance: { via: 'human-decision', refs: [], actor: 'config' },
378
+ scope: { projectKey },
379
+ supersedes: r.id,
380
+ withdrawn: true,
381
+ }, false);
382
+ }
383
+ }
384
+ /**
385
+ * Record that an approach was tried and failed.
386
+ *
387
+ * P11.21, and it is the reason memory matters most after Phase 9: a model with
388
+ * no recollection proposes the same failed approach again, and the loop-safety
389
+ * brake stops the machine without anybody learning why. This is what turns a
390
+ * repeated failure into something the NEXT plan can read.
391
+ *
392
+ * Keyed on the run, so two failures of one approach stay two records — that a
393
+ * thing failed twice is the whole signal, and collapsing them would erase it.
394
+ */
395
+ async function rememberFailedAttempt(store, projectKey, facts) {
396
+ if (store === null)
397
+ return;
398
+ await store.remember({
399
+ kind: 'attempt',
400
+ source: 'deterministic',
401
+ subject: facts.subject,
402
+ // The STEP, never the output. A digest identifies which failure it was
403
+ // without carrying a line of it — see `failureOutputDigest`.
404
+ content: `the ${facts.failureStep} gate refused this approach`,
405
+ provenance: { via: 'gate-result', refs: [facts.runId] },
406
+ scope: { projectKey, ...(facts.goalId === undefined ? {} : { goalId: facts.goalId }) },
407
+ distinct: facts.runId,
408
+ }, false);
409
+ }
410
+ async function memoryFor(store, git, scope,
411
+ /**
412
+ * The current project graph, when one could be established.
413
+ *
414
+ * 🔴 IT REORDERS, IT NEVER FILTERS — see `rankWithin`. Null means no graph was
415
+ * current, and the ranking is then exactly what it was before Phase 13, which
416
+ * is why this is a widening rather than a change of behaviour.
417
+ */
418
+ graph) {
419
+ if (store === null)
420
+ return null;
421
+ const tree = await git.headTreeSha().catch(() => null);
422
+ const records = [
423
+ ...store.current(tree, { kind: 'decision' }),
424
+ ...store.current(tree, { kind: 'fact' }),
425
+ ...store.current(tree, { kind: 'attempt', ...(scope.goalId === undefined ? {} : { goalId: scope.goalId }) }),
426
+ ...store.current(tree, { kind: 'inference' }),
427
+ ];
428
+ return (0, core_1.renderMemoryContext)((0, core_1.assembleMemoryContext)(records, undefined, relevantIds(graph, scope.goalId)));
429
+ }
430
+ /**
431
+ * Which remembered records the graph says are about the work in hand.
432
+ *
433
+ * Empty whenever there is no graph or no bound goal — an empty set makes the
434
+ * relevance clause a no-op rather than a special case, so there is one code path
435
+ * here and not two.
436
+ */
437
+ function relevantIds(graph, goalId) {
438
+ if (graph === null || graph === undefined || goalId === undefined)
439
+ return new Set();
440
+ const context = (0, core_1.contextForGoal)(graph, goalId);
441
+ if (!context.found)
442
+ return new Set();
443
+ /**
444
+ * All four kinds, because relevance is not the same question as standing. A
445
+ * hypothesis about the file being changed is more worth the model's attention
446
+ * than one about an unrelated module — it is still labelled a hypothesis, by
447
+ * the section it lands in, which this cannot touch.
448
+ */
449
+ return new Set([
450
+ ...context.decisions,
451
+ ...context.facts,
452
+ ...context.attempts,
453
+ ...context.inferences,
454
+ ]);
455
+ }
456
+ /**
457
+ * WHAT THE COMMITTED REPOSITORY SAYS ABOUT THIS GOAL'S WORK.
458
+ *
459
+ * 🔴 THIS IS THE PHASE'S PRODUCTION CONSUMER, AND IT IS ONE LINE FROM A PROMPT.
460
+ *
461
+ * Everything else in Phase 13 — the authority classes, the freshness guard, the
462
+ * bounded queries — exists so that this function can put structural claims in
463
+ * front of a model without any of them being mistakable for a guess. If this
464
+ * were not wired, the phase would be the thing it was written to avoid: correct,
465
+ * tested, and consumed by nothing.
466
+ *
467
+ * Null when no graph is current. A refusal renders nothing rather than an empty
468
+ * heading, for the reason `renderMemoryContext` returns null.
469
+ */
470
+ function structureFor(graph, goalId) {
471
+ if (graph === null || goalId === undefined)
472
+ return null;
473
+ return (0, core_1.renderGraphContext)((0, core_1.assembleGraphContext)((0, core_1.contextForGoal)(graph, goalId)));
474
+ }
475
+ function buildImplementerPrompt(item, attempt, cfg,
476
+ /**
477
+ * What a person refused, when this run is a resume after one.
478
+ *
479
+ * Rendered as labelled prose rather than as data, and that is a safety
480
+ * decision rather than a stylistic one: the transcript is scanned for a
481
+ * structured request for authority, and the scan takes the FIRST one it
482
+ * finds. A prompt that illustrated the shape of such a request would be a
483
+ * decoy that an echoing model files for real.
484
+ */
485
+ resolution,
486
+ /**
487
+ * What a person approved that has already been DONE, when this run did it.
488
+ *
489
+ * Prose for the same reason, and the reason bites harder here: this block
490
+ * describes a consequential act that SUCCEEDED, and a model that read it as
491
+ * a template and echoed the shape back would be asking for that act again.
492
+ */
493
+ governed,
494
+ /**
495
+ * What this project has already established, as labelled prose.
496
+ *
497
+ * Prose for the same reason the two blocks above are, and the reasoning is
498
+ * Phase 7's: a declaration of authority is a SHAPE on a structured channel,
499
+ * never a phrase, so nothing written here can become a request. What it must
500
+ * not do is show a TEMPLATE of that shape, which an echoing model could fill
501
+ * in for real — so memory is rendered as bulleted claims, never as data.
502
+ *
503
+ * Already sectioned and budgeted by `assembleMemoryContext`; this builder
504
+ * neither ranks nor trims, because one that re-decided what fits would be a
505
+ * second place the budget lives.
506
+ */
507
+ memory,
508
+ /**
509
+ * What the committed repository establishes about this work, from the graph.
510
+ *
511
+ * 🔴 A SEPARATE PARAMETER FROM `memory`, AND SEPARATE ALL THE WAY TO THE
512
+ * PROMPT. Concatenating them here would put "the repository says these files
513
+ * import you" under the same heading as "a person decided you must not
514
+ * force-push" — two different authorities, and the whole of Phase 13 is that
515
+ * a reader can always tell which one is speaking.
516
+ */
517
+ structure,
518
+ /**
519
+ * What the previous attempt did and what refused it, when this run saw it.
520
+ *
521
+ * 🔴 THE RETRY USED TO GET ONE LINE AND A CLEAN TREE. "gate red: step
522
+ * typecheck failed" was the whole briefing, and the work from the round
523
+ * before had been reset away without a word about it. A worker cannot
524
+ * repair what it has not been shown; it starts over. This renders the
525
+ * retained evidence — the failing step's output, the manifest, the patch or
526
+ * the fact that it was restored into the tree — as labelled prose.
527
+ *
528
+ * Prose, and never a template, for the same reason every block above is:
529
+ * the step output and the patch are fenced as text the model is to READ,
530
+ * and nothing here shows the shape of a structured request for authority.
531
+ */
532
+ repair,
533
+ /**
534
+ * WHAT THE PERSON ACTUALLY ASKED FOR, WHICH NEVER GOT THIS FAR.
535
+ *
536
+ * 🔴 THE IMPLEMENTER WAS HANDED AN ITEM TITLE. A person types a goal of up to
537
+ * two thousand characters; the brain compiles it into items; and this builder
538
+ * emitted `item.title` and nothing else — so everything they wrote about what
539
+ * they want, why, and what would count as done stopped at the compile step.
540
+ * The capability audit's E-01, and the distance between it and what the
541
+ * product promises is the whole reason M8 exists: eleven cycles of M6 made
542
+ * the AUTHORITY to pursue a goal safe, while the thing being pursued was not
543
+ * being carried to the model doing the work.
544
+ *
545
+ * ⚠️ IT WAS ALWAYS ONE PROPERTY AWAY. The call site reads
546
+ * `binding.goal.id` off a `Goal` whose `text` is, in core's words, "the
547
+ * user's own wording, preserved verbatim". Nothing had to be carried here;
548
+ * the id was taken and the text was left behind.
549
+ *
550
+ * ⚠️ PROSE, AND NEVER A TEMPLATE, for the reason every block above says it:
551
+ * a declaration of authority is a SHAPE on a structured channel, never a
552
+ * phrase, and a prompt that showed that shape would be a decoy an echoing
553
+ * model could fill in for real. A goal is exactly where a person might write
554
+ * a sentence that READS like an instruction — "and then push it" — so the
555
+ * block introduces the text as something THEY wrote, and carries no
556
+ * structured shape at all.
557
+ *
558
+ * ⚠️ NOT TRIMMED HERE. The server caps a goal at `MAX_GOAL_TEXT` where it is
559
+ * created, and a goal reaches this machine only through the server. A second
560
+ * trim here would be a second place the budget lives — the same reason
561
+ * `memory` is neither ranked nor trimmed by this function.
562
+ */
563
+ goal) {
564
+ const retry = repair === undefined && attempt > 0 && item.lastFailure
565
+ ? `\n## Why the last attempt was thrown away\n\n${item.lastFailure}\n\nDo not repeat it.\n`
566
+ : '';
567
+ const repairing = repair === undefined ? '' : describeRepair(repair);
568
+ const mustPass = cfg.gates.length === 0
569
+ ? ''
570
+ : `\n## What must pass before the change is kept\n\n${cfg.gates
571
+ .map((g) => `- ${g.name}: \`${g.run}\`${g.required ? ' (required)' : ''}`)
572
+ .join('\n')}\n\nThe runner executes these after you stop. A change that does not pass them is discarded.\n`;
573
+ const answered = resolution === undefined
574
+ ? ''
575
+ : `\n## What a person already decided\n\n${(0, approval_resolution_1.describeResolution)(resolution)}\n`;
576
+ const carriedOut = governed === undefined
577
+ ? ''
578
+ : `\n## What has already been done for you\n\n${(0, governed_run_1.describeGovernedResult)(governed)}\n`;
579
+ // Null when nothing has been remembered yet — an empty heading would tell the
580
+ // model that Looply looked and found nothing, which is false on a new project.
581
+ const known = memory === undefined || memory === null
582
+ ? ''
583
+ : `\n## What this project has already established\n\n${memory}\n`;
584
+ /**
585
+ * Null when no graph was current — which is the honest rendering of "Looply
586
+ * could not establish the structure this time", and is not the same claim as
587
+ * "this file is connected to nothing".
588
+ */
589
+ const shape = structure === undefined || structure === null
590
+ ? ''
591
+ : `\n## How this code is connected\n\n${structure}\n`;
592
+ /**
593
+ * Empty when this round is not working on a goal — a scan-seeded item, a
594
+ * manual run, a goal that lapsed — for the reason `memory` is empty when
595
+ * nothing is remembered: a heading with nothing under it would tell the model
596
+ * that Looply looked and found nothing, which is a different claim.
597
+ *
598
+ * The sentence around it names whose words these are. A model that cannot
599
+ * tell the person's wording from the runner's own instructions is one
600
+ * sentence away from treating the former as the latter.
601
+ */
602
+ const why = goal === undefined || goal === null || goal.trim() === ''
603
+ ? ''
604
+ : `\n## What this is for\n\nThe person who set this work going wrote this, and it is their wording, not Looply's:\n\n${goal}\n\nThe task above is one step towards it. Do that step; do not try to do the rest of it here.\n`;
605
+ return `You are one round of looply on the ${cfg.project.name} repository.
606
+
607
+ Do exactly this one task, then stop.
608
+
609
+ ## The task
610
+
611
+ ${item.title}
612
+
613
+ ## Where it is
614
+
615
+ ${evidenceLines(item.evidence)}
616
+ ${why}${known}${shape}${retry}${repairing}${answered}${carriedOut}${mustPass}
617
+ ## Scope
618
+
619
+ You may edit: ${cfg.scope.free.join(', ') || '(nothing declared)'}
620
+ Additive only (new files): ${cfg.scope.additive.join(', ') || '(none)'}
621
+ Never touch: ${cfg.rails.forbiddenPaths.join(', ') || '(no rails declared)'}
622
+
623
+ If the task cannot be done without touching a railed path, do not touch it —
624
+ report it as blocked with the path and the reason, and change nothing.
625
+
626
+ ## Rules
627
+
628
+ - One concern. Keep the diff tight.
629
+ - Add or extend a unit test covering the new behaviour. Assert the requirement,
630
+ not the implementation.
631
+ - Never run git. The runner handles branching, committing and discarding.
632
+ - Do not add a dependency unless this task explicitly calls for one.
633
+
634
+ ## When you are done
635
+
636
+ Print exactly one JSON object, on its own line, and nothing after it:
637
+
638
+ {"itemId":"${item.id}","outcome":"implemented","filesTouched":["..."],"testsAdded":0,"note":"one sentence","blocked":null}
639
+
640
+ If you were blocked by a rail, use:
641
+
642
+ {"itemId":"${item.id}","outcome":"blocked","filesTouched":[],"testsAdded":0,"note":"","blocked":{"path":"...","reason":"..."}}
643
+ `;
644
+ }
645
+ function openGoalServer(cwd, goals, log) {
646
+ let identity;
647
+ const store = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)());
648
+ try {
649
+ identity = store.load();
650
+ }
651
+ catch {
652
+ return null;
653
+ }
654
+ if (!identity)
655
+ return null;
656
+ const projectId = store.projectFor((0, machine_store_1.repoPathHash)(cwd, (t) => (0, node_crypto_1.createHash)('sha256').update(t, 'utf8').digest('hex')));
657
+ if (!projectId)
658
+ return null;
659
+ const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl }).withToken(identity.token);
660
+ return {
661
+ projectId,
662
+ approvals: { createApproval: (p, input) => client.createApproval(p, input) },
663
+ resolution: { projectApproval: (p, id) => client.projectApproval(p, id) },
664
+ gate: { projectApprovals: (p, goal, limit) => client.projectApprovals(p, goal, limit) },
665
+ // ...and the governed surface, assembled where the vocabulary belongs.
666
+ governed: (0, governed_run_1.governedServerOf)(client),
667
+ sources: { sourceDetail: (p, id) => client.sourceDetail(p, id) },
668
+ servings: { forGoal: async (p, goalId) => (await client.goalServings(p, goalId)).servings },
669
+ questions: {
670
+ ask: async (body) => ({ id: (await client.askQuestion(body)).id }),
671
+ read: async (p, id) => (await client.questionById(p, id)).question,
672
+ },
673
+ edges: {
674
+ read: async (p, goalId) => {
675
+ const found = (await client.projectGoals(p)).goals.find((g) => g.id === goalId);
676
+ return found === undefined ? null : { version: found.version, dependsOn: found.dependsOn ?? [] };
677
+ },
678
+ setDependsOn: async (p, goalId, version, dependsOn, executionId) => {
679
+ await client.applyGovernedGoalEdge(p, executionId, { goalId, version, dependsOn });
680
+ },
681
+ },
682
+ refresh: async () => {
683
+ try {
684
+ const r = await client.projectGoals(projectId);
685
+ // A 200 is not an answer. A captive portal or a proxy interstitial
686
+ // arrives here as a successful response, and writing its body over the
687
+ // mirror would erase the very state this is trying to read.
688
+ const body = r;
689
+ if (typeof body !== 'object' || body === null || !Array.isArray(body.goals)) {
690
+ log('goal : the server answered with something that is not a goal list');
691
+ return 'unreachable';
692
+ }
693
+ await goals.replace(body.goals);
694
+ return 'ok';
695
+ }
696
+ catch (e) {
697
+ log(`goal : could not re-read your goals (${e.message})`);
698
+ return 'unreachable';
699
+ }
700
+ },
701
+ };
702
+ }
703
+ /**
704
+ * Assemble and run. Returns the summary the caller prints; every transition is
705
+ * already on disk in the ledger by the time this resolves.
706
+ */
707
+ async function runLoop(opts) {
708
+ const { cwd, log } = opts;
709
+ // Reassigned once the phone's settings are read; see below.
710
+ let config = opts.config;
711
+ /**
712
+ * TWO DIRECTORIES, AND WHICH ONE A STORE GETS IS ITS INVARIANT'S ANSWER.
713
+ *
714
+ * `stateDir` is this EXECUTION's — one writer, one checkout. `projectDir` is
715
+ * the PROJECT's — the main worktree's, shared by every worktree of this
716
+ * repository. In an ordinary checkout they are the same path, so nothing about
717
+ * a normal `looply run` changes.
718
+ */
719
+ const stateDir = (0, git_driver_1.executionStateDir)(cwd);
720
+ const projectStateDirPath = (0, git_driver_1.projectStateDir)(cwd);
721
+ const ledger = new ledger_store_1.LedgerStore(stateDir, ledger_store_1.nodeLedgerFs);
722
+ const queue = new queue_store_1.QueueStore(stateDir, ledger_store_1.nodeLedgerFs);
723
+ const evidence = new evidence_cache_1.EvidenceCache(stateDir, ledger_store_1.nodeLedgerFs);
724
+ // PROJECT-SCOPED: the one channel the daemon, the run and the controller share
725
+ // about whether a person said stop. Per-worktree, `looply stop` would reach
726
+ // one execution and silently miss the rest.
727
+ const projectControl = new control_file_1.ControlStore(projectStateDirPath, ledger_store_1.nodeLedgerFs);
728
+ // Which of a person's answers have already been handed to a run. The daemon
729
+ // writes these at the Start boundary; a run only ever reads them, to tell a
730
+ // refusal nobody has heard yet from one this very run was told about.
731
+ //
732
+ // 🔴 PROJECT-SCOPED, BECAUSE THE DAEMON THAT WRITES THEM IS. The daemon
733
+ // claims an answer under `projectStateDir`; this read it from the EXECUTION
734
+ // state, so a run placed in another checkout looked in a directory the daemon
735
+ // never wrote to, saw no claim, and treated an answer it had just been handed
736
+ // as one nobody had heard. Same split as the start marks, same fix.
737
+ const resumes = new approval_resumes_1.ApprovalResumes(projectStateDirPath, start_attempts_1.nodeAttemptFs);
738
+ /**
739
+ * The safety history, opened once here and READ ONCE at the fence below.
740
+ *
741
+ * Null when it could not be read, and the fence then lets the run through —
742
+ * which is the opposite of the daemon's rule for the same file, and
743
+ * deliberately so. The daemon is deciding whether to spend a person's night
744
+ * unattended and must fail closed; this is a person at a terminal who has
745
+ * typed a command, and refusing them because a JSON file is corrupt would
746
+ * strand them with no way to work. The daemon's brake is still on.
747
+ */
748
+ const safety = await (async () => {
749
+ // MACHINE-SCOPED, keyed by the repository's identity. The history is
750
+ // bucketed on goalId alone, so the directory it sits in IS the other half
751
+ // of its key: a second worktree would be a fresh history, and since both
752
+ // detectors need three observations while a run contributes one,
753
+ // per-worktree histories mean they can never fire. Phase 1 fixed that by
754
+ // moving it to the project state directory; Phase 2 moved it out of the
755
+ // repository, where a re-clone or a `git clean -xfd` cannot take a live
756
+ // hold with it. See rails-home.ts.
757
+ const store = (0, rails_stores_1.railsSafetyStore)(cwd);
758
+ return (await store.open()) === 'ok' ? store : null;
759
+ })();
760
+ /**
761
+ * Project memory, opened the same way and for the same reason: a person at a
762
+ * keyboard is not refused because a JSON file is unreadable. Null simply
763
+ * means this run plans and works without what the project has learned — which
764
+ * is exactly how every run behaved before Phase 11.
765
+ */
766
+ const memoryKey = (0, project_memory_store_1.projectKeyFor)(cwd, (t) => (0, node_crypto_1.createHash)('sha256').update(t, 'utf8').digest('hex'));
767
+ const memoryStore = await (async () => {
768
+ const store = new project_memory_store_1.ProjectMemoryStore(stateDir, memoryKey, ledger_store_1.nodeLedgerFs);
769
+ return (await store.open()) === 'ok' ? store : null;
770
+ })();
771
+ // The person's decisions, before anything plans or works. A decision read
772
+ // after the plan is a constraint applied too late.
773
+ await rememberHumanDecisions(memoryStore, config, memoryKey);
774
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(cwd));
775
+ /**
776
+ * What the repository IS, established deterministically and remembered.
777
+ *
778
+ * 🔴 THIS IS THE HALF EACH OF THE LAST TWO PHASES WAS MISSING. Phase 11 froze
779
+ * with `fact` having no production producer; Phase 12 built an index nothing
780
+ * consumed. Each was the other's missing piece, and both were recorded as
781
+ * known limits rather than hidden — this is them being closed.
782
+ *
783
+ * Cached by tree, so the cost is paid once per revision rather than once per
784
+ * run. A failure here is not a reason to refuse a run: the index is a
785
+ * convenience for planning, not an authority over anything, so it fails
786
+ * silently and the run proceeds exactly as it did before Phase 12.
787
+ */
788
+ await (async () => {
789
+ if (memoryStore === null)
790
+ return;
791
+ try {
792
+ const tree = await git.headTreeSha();
793
+ const indexStore = new project_index_store_1.ProjectIndexStore(stateDir, ledger_store_1.nodeLedgerFs);
794
+ /**
795
+ * ⚠️ THE PACKAGE SET IS PART OF THE KEY, NOT JUST OF THE BUILD. Passing
796
+ * it is what stops a config edit — which no tree sha covers — being
797
+ * served a snapshot built under the previous workspace list. See
798
+ * `ProjectIndexStore.load`.
799
+ */
800
+ const packages = (0, index_for_head_1.workspacePackages)(config);
801
+ let snapshot = await indexStore.load(tree, packages.map((pkg) => pkg.name));
802
+ if (snapshot === null) {
803
+ snapshot = await (0, project_index_builder_1.buildProjectIndex)({ git, packages, treeSha: tree });
804
+ await indexStore.save(snapshot);
805
+ }
806
+ for (const draft of (0, project_index_facts_1.factsFromIndex)(snapshot, memoryKey)) {
807
+ await memoryStore.remember(draft, false);
808
+ }
809
+ }
810
+ catch {
811
+ // Deliberately silent. Nothing downstream depends on the index existing,
812
+ // and a run that refused to start because a derived cache failed would be
813
+ // trading a real capability for a convenience.
814
+ }
815
+ })();
816
+ await ledger.open();
817
+ await queue.open();
818
+ await evidence.open();
819
+ // The goal mirror the daemon keeps. Read here rather than fetched, because
820
+ // the daemon has almost certainly already pulled the version the phone was
821
+ // looking at, and a run that phoned home before it could begin would make
822
+ // the network a dependency of doing any work at all.
823
+ // PROJECT-SCOPED, read-only here; the daemon is the sole writer.
824
+ const goals = new goal_store_1.GoalStore(projectStateDirPath, ledger_store_1.nodeLedgerFs);
825
+ await goals.load();
826
+ const goalMirror = { get: (id) => goals.get(id) };
827
+ /**
828
+ * HOW STRUCTURE, KNOWLEDGE AND WORK RELATE — asked fresh, every time.
829
+ *
830
+ * 🔴 CONSTRUCTED HERE BECAUSE IT NEEDS ALL FIVE STORES, AND CONSULTED LATER
831
+ * BECAUSE THE WORLD MOVES WHILE A RUN WORKS. `round-runner` commits inside
832
+ * the loop below, so the committed tree is not the tree this line measured.
833
+ * The runtime asks that question again at every consumption and rebuilds
834
+ * when the answer changed — which is the half Phase 12 could not do, and why
835
+ * its index-derived facts fall silent after a run’s first commit.
836
+ *
837
+ * Nothing downstream is allowed to depend on it: every consumer treats null
838
+ * as "render nothing", exactly as it treated a missing memory store before.
839
+ */
840
+ const graphRuntime = new project_graph_runtime_1.ProjectGraphRuntime({
841
+ projectKey: memoryKey,
842
+ headTreeSha: () => git.headTreeSha(),
843
+ indexFor: async (tree) => {
844
+ const store = new project_index_store_1.ProjectIndexStore(stateDir, ledger_store_1.nodeLedgerFs);
845
+ const packages = (0, index_for_head_1.workspacePackages)(config);
846
+ // Same key as the block above, and for the same reason.
847
+ const cached = await store.load(tree, packages.map((pkg) => pkg.name));
848
+ if (cached !== null)
849
+ return cached;
850
+ const built = await (0, project_index_builder_1.buildProjectIndex)({
851
+ git,
852
+ packages,
853
+ treeSha: tree,
854
+ });
855
+ await store.save(built);
856
+ return built;
857
+ },
858
+ memory: () => (memoryStore === null ? [] : memoryStore.history()),
859
+ operational: () => ({
860
+ goals: goals.all().map((g) => ({ id: g.id, text: g.text, version: g.version })),
861
+ items: queue.list().map((i) => ({
862
+ id: i.id,
863
+ title: i.title,
864
+ goalId: i.goalId ?? null,
865
+ evidence: i.evidence.map((e) => ({ path: e.path })),
866
+ })),
867
+ }),
868
+ });
869
+ /**
870
+ * One question, asked once per consumption point, so a failure to establish
871
+ * the graph costs nothing and changes nothing. A run that cannot build one
872
+ * behaves exactly as every run behaved before this phase.
873
+ */
874
+ const currentGraph = async () => graphRuntime.current().catch(() => null);
875
+ /**
876
+ * IS THIS STILL THE AUTHORITY? Asked of the FILE, not of what we remember.
877
+ *
878
+ * `goals.get` answers from an array this process loaded once, which is right
879
+ * for the cheap questions a run asks constantly and wrong for this one:
880
+ * comparing that frozen snapshot against a binding taken from the same
881
+ * snapshot compares a value to itself and can never fail.
882
+ *
883
+ * Both compilations need it, and for the same reason. Each hands a goal's
884
+ * wording to an agent for up to ten minutes, then writes durable work stamped
885
+ * with the version it started under — while the daemon rewrites goals.json on
886
+ * its own poll cadence, holding no lease, so a person amending or abandoning
887
+ * the goal in that window is invisible to a run that only remembers.
888
+ *
889
+ * ONE function, because this was fixed for the adaptive compile and left
890
+ * undone for the first one, which is the same mistake this phase has now made
891
+ * four times: correcting a path and not its twin.
892
+ *
893
+ * Still local-first, and no server call. A machine whose mirror has not yet
894
+ * learned of a newer goal may go on working the one it knows; what this
895
+ * refuses is going on after the newer authority HAS arrived here.
896
+ */
897
+ const freshAuthority = async (goalId, boundVersion, boundText) => (0, goal_ownership_1.stillTheAuthority)(await goals.readFresh(), goalId, boundVersion, boundText);
898
+ // Built without starting a sync daemon: this needs one GET, not a flush loop,
899
+ // and the sync attaches much further down once the lease is held.
900
+ const goalServer = openGoalServer(cwd, goals, log);
901
+ // Resolved BEFORE the lease: this is read-only and may go to the network,
902
+ // and holding the working tree during a call that might time out would block
903
+ // every other run for no reason. A refusal here has taken nothing.
904
+ const binding = await (0, goal_ownership_1.resolveGoalBinding)({
905
+ ...(opts.goalId === undefined ? {} : { goalId: opts.goalId }),
906
+ ...(opts.goalVersion === undefined ? {} : { goalVersion: opts.goalVersion }),
907
+ // The SERVER's id for this repo, not the config's. They are different
908
+ // strings — the config's is a local name and every goal carries a `prj_`
909
+ // id — so checking against the wrong one would refuse every goal as
910
+ // belonging to somewhere else.
911
+ projectId: goalServer?.projectId ?? config.project.id ?? 'local',
912
+ mirror: goalMirror,
913
+ ...(goalServer === null ? {} : { refresh: goalServer.refresh }),
914
+ });
915
+ /**
916
+ * What a person has attested about the requirements this goal names, read off
917
+ * the binding this run is bound to.
918
+ *
919
+ * A function rather than a value because both endings ask for it at the
920
+ * moment they render, and because a goal can be amended while a run is going:
921
+ * asking then means the sentence describes the goal the reader is being told
922
+ * about. Null for a run with no goal, and for a goal that names no
923
+ * requirements — and then both endings are byte-identical to before.
924
+ */
925
+ const attestedNow = () => binding.kind === 'bound' ? (0, core_3.attestedStanding)(binding.goal) : null;
926
+ /**
927
+ * THE ITEM→REQUIREMENT CLAIMS THIS PLAN MAKES, validated before they are
928
+ * recorded.
929
+ *
930
+ * 🔴 THE MODEL'S PROPOSAL IS UNTRUSTED DATA ALL THE WAY TO ONE VALIDATOR.
931
+ * `normalizeStep` carries `serves` through without inspecting it;
932
+ * `produceClaims` filters step names against THIS project's gate — the one
933
+ * check core cannot make, because the gate configuration lives here — and
934
+ * then hands the remainder to `servedRequirementTrouble`, the same pure
935
+ * function the server's route calls. Nothing else decides.
936
+ *
937
+ * ⚠️ EVERY FAILURE PATH RECORDS NO CLAIMS, NEVER PARTIAL ONES. An unreadable
938
+ * source, an unbound goal or a server this run cannot reach all answer the
939
+ * empty list — because a claim silently missing is a requirement that blocks
940
+ * at the coverage gate, which is visible, while a claim wrongly recorded is
941
+ * evidence that is not.
942
+ *
943
+ * ⚠️ AND NO FALLBACK. A goal's requirements are NOT copied onto its items when
944
+ * the model named none: that would make every item claim everything and the
945
+ * per-requirement receipt would mean exactly what the whole-tree one already
946
+ * meant.
947
+ */
948
+ const claimsFor = async (steps, items, goalLinks) => {
949
+ if (goalServer === null || goalLinks.length === 0)
950
+ return [];
951
+ if (!steps.some((s) => s.serves !== undefined))
952
+ return [];
953
+ const sources = await (0, requirement_claims_1.linkableSourcesForGoal)(goalLinks, async (sourceId) => goalServer.sources.sourceDetail(projectId, sourceId));
954
+ if (sources === null) {
955
+ log('compile : a linked source could not be read, so no requirement claims were recorded');
956
+ return [];
957
+ }
958
+ const produced = (0, requirement_claims_1.produceClaims)({
959
+ steps,
960
+ itemIds: items.map((i) => i.id),
961
+ goalLinks,
962
+ sources,
963
+ gateStepNames: new Set(config.gates.map((g) => g.name)),
964
+ });
965
+ // Named, never silent: a refused claim is a model proposal this project
966
+ // declined, and the person reading the log is the one who can fix it.
967
+ for (const d of produced.dropped)
968
+ log(`compile : claim refused — ${d.because}`);
969
+ if (produced.servings.length > 0) {
970
+ log(`compile : ${produced.servings.length} item(s) claim a requirement`);
971
+ }
972
+ return [...produced.servings];
973
+ };
974
+ /**
975
+ * WHAT THIS DELIVERY FINISHED, FOR THE LANE TO JUDGE LATER.
976
+ *
977
+ * The claims say what each item was FOR; this says which of them the delivery
978
+ * actually finished, and at which tip. The lane cannot re-derive either — no
979
+ * commit trailer records an item id, and the run registry is released on a
980
+ * clean exit — so it is written where KEY 2 has already answered both, rather
981
+ * than derived a second time somewhere that could disagree.
982
+ *
983
+ * ⚠️ IT IS PROVENANCE, NOT ACCEPTANCE. `coverageNow` is this worker's verdict
984
+ * about its own checkout; the lane discards that and recomputes from its own
985
+ * gate on the commit it publishes. What travels is the claim.
986
+ *
987
+ * ⚠️ AN ARROW, LIKE `coverageNow`, AND FOR THE SAME TYPE-LEVEL REASON.
988
+ * `key2Saw` is a `let` filled inside a callback, so in the straight-line body
989
+ * TypeScript's flow analysis still holds it at its `null` initializer and
990
+ * narrows `!== null` to `never`. Read across a closure boundary it has its
991
+ * declared type, which is the honest one — the alternative was a non-null
992
+ * assertion, and that would be a claim where this needs a check.
993
+ */
994
+ /**
995
+ * 🔴 THE BRANCH THAT DELIVERED, NOT THIS START'S (F20). The goal-met question is
996
+ * asked by a Start whose plan had nothing left to pick, and that Start refuses
997
+ * before its runner ever creates `branch`. KEY 2's confirming round names the run
998
+ * that delivered; the lane-claims ledger names the branch that run cut
999
+ * (`deliveringBranchOf`). The checked-out branch is the fallback, `branch` the last.
1000
+ *
1001
+ * ⚠️ AN ARROW, for the same flow-analysis reason as `recordDelivery` below.
1002
+ */
1003
+ const deliveringBranch = async () => {
1004
+ if (deliveringRunId === null)
1005
+ return branch;
1006
+ const cut = (0, run_delivery_1.deliveringBranchOf)((0, lane_claims_1.readLaneClaims)(cwd), deliveringRunId);
1007
+ if (cut !== null)
1008
+ return cut;
1009
+ try {
1010
+ const current = await git.currentBranch();
1011
+ // F33: after a goal-met Start detached HEAD there is no branch to name; this Start's own is the honest fallback.
1012
+ return current === '(detached)' || current === '(unknown)' ? branch : current;
1013
+ }
1014
+ catch {
1015
+ return branch;
1016
+ }
1017
+ };
1018
+ const recordDelivery = async (at, goalVersion) => {
1019
+ if (key2Saw === null)
1020
+ return;
1021
+ // F31 (soun-4): only work the confirmed head's history contains — the rounds of THIS goal's items.
1022
+ const confirmedAt = judged.kind === 'carried' ? judged.of : await git.headSha();
1023
+ const mineIds = new Set(queue.list().filter((i) => i.goalId === at.goalId).map((i) => i.id));
1024
+ const rounds = await (0, round_records_1.readRoundRecords)(stateDir, ledger_store_1.nodeLedgerFs);
1025
+ const ours = rounds.ok ? rounds.rounds.filter((r) => mineIds.has(r.itemId)) : [];
1026
+ const delivered = rounds.ok ? (0, run_delivery_1.onThisBranch)([...key2Saw.delivered], ours, await (0, run_delivery_1.reachableFrom)(git, confirmedAt, ours)) : [...key2Saw.delivered];
1027
+ (0, lane_claims_1.recordLaneClaim)(cwd, {
1028
+ schema: lane_claims_1.LANE_CLAIMS_SCHEMA,
1029
+ kind: 'delivery',
1030
+ at: new Date().toISOString(),
1031
+ branch: await deliveringBranch(),
1032
+ runId: deliveringRunId ?? runId,
1033
+ goalId: at.goalId,
1034
+ goalVersion,
1035
+ planRevision: at.planRevision ?? core_1.FIRST_PLAN_REVISION,
1036
+ // KEY 2's own sha — for a carried commit, the round the lane carried (F27).
1037
+ commitSha: judged.kind === 'carried' ? judged.of : await git.headSha(),
1038
+ ...(judged.kind === 'carried' ? { judgedAt: judged.sha } : {}),
1039
+ delivered,
1040
+ acceptanceDigest: (0, core_3.acceptanceMapDigest)(config.gates),
1041
+ });
1042
+ };
1043
+ /**
1044
+ * D3 — the fourth key, computed at the moment the claim would be made.
1045
+ *
1046
+ * 🔴 THE DIFFERENCE BETWEEN D3 IMPLEMENTED AND D3 WIRED. `coverageGate` is a
1047
+ * pure fold; without a caller it decides nothing, and a rule nothing consults
1048
+ * is the shape this programme keeps writing tests about.
1049
+ *
1050
+ * ⚠️ `deliveredUnderReceipt: true` IS NOT AN ASSUMPTION. This is only ever
1051
+ * asked for a `satisfied` decision, and `satisfied` is reachable only when
1052
+ * KEY 2 turned — a decision whose KEY 2 failed is `unconfirmed`, a different
1053
+ * kind entirely. So the receipt is already established by the time this runs,
1054
+ * and re-deriving it here would be a second implementation of the same
1055
+ * question that could disagree with the first.
1056
+ *
1057
+ * ⚠️ UNDEFINED WHEN IT CANNOT BE ASKED, WHICH IS NOT THE SAME AS COVERED. No
1058
+ * server, no linked requirements, or a source the server could not answer for
1059
+ * all return undefined, and `describeAdaptive` then renders exactly what it
1060
+ * always did. What it must never do is return an empty gate, because an empty
1061
+ * gate says `ok` — granting coverage on the strength of a failed lookup.
1062
+ */
1063
+ const coverageNow = async () => {
1064
+ if (binding.kind !== 'bound' || goalServer === null)
1065
+ return undefined;
1066
+ const links = binding.goal.requirements ?? [];
1067
+ if (links.length === 0)
1068
+ return undefined;
1069
+ const classifications = new Map();
1070
+ const dispositions = new Map();
1071
+ const attestations = new Map();
1072
+ let measuredTree = null;
1073
+ for (const sourceId of new Set(links.map((l) => l.sourceId))) {
1074
+ let detail;
1075
+ try {
1076
+ detail = await goalServer.sources.sourceDetail(projectId, sourceId);
1077
+ }
1078
+ catch {
1079
+ // A source that could not be read is a coverage question this run
1080
+ // cannot answer. Answering "covered" would be the lie; answering
1081
+ // "blocked" for every one of its links would invent a measurement.
1082
+ return undefined;
1083
+ }
1084
+ const key = (ordinal) => `${sourceId}#${ordinal}`;
1085
+ for (const c of detail.classifications)
1086
+ classifications.set(key(c.ordinal), c);
1087
+ for (const d of detail.dispositions)
1088
+ dispositions.set(key(d.ordinal), d);
1089
+ for (const a of detail.attestations)
1090
+ attestations.set(key(a.ordinal), a);
1091
+ if (detail.reconciledAt !== null)
1092
+ measuredTree = detail.reconciledAt.treeSha;
1093
+ }
1094
+ /**
1095
+ * 🔴 THE FOURTH KEY'S STRONGER HALF, AND THE FIRST TIME IT CAN BE EARNED.
1096
+ *
1097
+ * `receiptedRequirements` has had a place in the gate and no producer:
1098
+ * every measured grant came back `measured-under-whole-tree-receipt`
1099
+ * because nothing recorded what an item was FOR. The claims are on the
1100
+ * server; what the delivery did is what KEY 2 saw; the fold is the only
1101
+ * thing that decides which claims survive both.
1102
+ *
1103
+ * ⚠️ A SERVER THAT CANNOT ANSWER LEAVES THE SET EMPTY, WHICH GRANTS
1104
+ * NOTHING EXTRA. It is the weaker word again, never a stronger one on a
1105
+ * failed lookup — the same rule the whole function already follows.
1106
+ */
1107
+ let receipted = new Set();
1108
+ if (key2Saw !== null) {
1109
+ try {
1110
+ const rows = await goalServer.servings.forGoal(projectId, binding.goal.id);
1111
+ const byItem = new Map();
1112
+ for (const r of rows) {
1113
+ const list = byItem.get(r.itemId) ?? [];
1114
+ list.push({
1115
+ sourceId: r.sourceId,
1116
+ ordinal: r.ordinal,
1117
+ ...(r.row === undefined ? {} : { row: r.row }),
1118
+ provenBy: r.provenBy,
1119
+ });
1120
+ byItem.set(r.itemId, list);
1121
+ }
1122
+ const fold = (0, core_3.requirementReceipts)({
1123
+ servings: [...byItem].map(([itemId, serves]) => ({ itemId, serves })),
1124
+ deliveredItems: key2Saw.delivered,
1125
+ executedChecks: key2Saw.checks,
1126
+ });
1127
+ // A proven table row is receipted under its own address (F24); the gate never lets it speak for its statement.
1128
+ receipted = new Set([...fold.receipted, ...fold.receiptedRows]);
1129
+ }
1130
+ catch {
1131
+ // Unreadable claims prove nothing either way. An empty set is the
1132
+ // honest input: the gate then says `measured-under-whole-tree-receipt`
1133
+ // in simple mode and blocks in strict, which is what "we could not
1134
+ // check" should do in each.
1135
+ }
1136
+ }
1137
+ return (0, core_3.coverageGate)({
1138
+ links,
1139
+ classifications,
1140
+ dispositions,
1141
+ attestations,
1142
+ measuredTree,
1143
+ deliveredUnderReceipt: true,
1144
+ receiptedRequirements: receipted,
1145
+ /**
1146
+ * 🔴 STRICT IS PROGRAMME MODE, AND SIMPLE IS EVERYTHING ELSE.
1147
+ *
1148
+ * A machine running as a programme decides its own completions with
1149
+ * nobody reading each one, so a green tree around an unclaimed
1150
+ * requirement must not settle it. Under a person's explicit Start the
1151
+ * answer is unchanged, byte for byte — which is the "preserve D3,
1152
+ * preserve Simple Mode" half of §6.B, and the reason this is a mode
1153
+ * rather than a new rule for everybody.
1154
+ */
1155
+ mode: (0, programme_mode_1.programmeIsOn)(config) ? 'strict' : 'simple',
1156
+ });
1157
+ };
1158
+ if (binding.kind === 'refused') {
1159
+ // The binding says WHICH refusal it is; nothing here reads its sentence.
1160
+ throw new RunRefused(binding.reason, binding.lapse === 'abandoned' ? 'goal_abandoned' : 'goal_superseded');
1161
+ }
1162
+ if (binding.kind === 'retry')
1163
+ throw new Error(binding.reason);
1164
+ /**
1165
+ * The version AND THE REVISION are part of the scope.
1166
+ *
1167
+ * A run works the plan compiled for the goal AS IT IS NOW, plus anything a
1168
+ * person adopted by hand. A previous version's plan stays owned and durable
1169
+ * and is simply not this run's to work — and the same is now true one level
1170
+ * down, of a previous REVISION of the current version.
1171
+ *
1172
+ * This was revision-blind, and the asymmetry it created is why 6D0 blocked:
1173
+ * selection saw every revision as current while KEY 2 asked only about the
1174
+ * newest, so a run could deliver revision 1 work that the completion check
1175
+ * could not see. Under one explicit Start that cost a person one confusing
1176
+ * refusal. Under an autonomous controller it is a dead end — Starts spent
1177
+ * delivering work that can never satisfy the goal.
1178
+ *
1179
+ * `FIRST_PLAN_REVISION` when the goal owns nothing yet: this run is about to
1180
+ * compile revision 1, and its rows have to be selectable when it does. It is
1181
+ * the MAXIMUM present otherwise, which is what makes a "future" revision
1182
+ * unreachable by construction rather than by luck.
1183
+ */
1184
+ let scope = binding.kind === 'bound'
1185
+ ? {
1186
+ goalId: binding.goal.id,
1187
+ goalVersion: binding.goal.version,
1188
+ planRevision: Math.max((0, goal_ownership_1.currentPlanRevision)(queue.list(), binding.goal.id, binding.goal.version), core_1.FIRST_PLAN_REVISION),
1189
+ }
1190
+ : null;
1191
+ if (binding.kind === 'bound') {
1192
+ log(`goal : ${binding.goal.text}`);
1193
+ }
1194
+ // Scoped from here on. `list()` is filtered and nothing else is, so a run
1195
+ // still reads and writes any item it is holding — see `scopedQueue`.
1196
+ let runQueue = (0, goal_ownership_1.scopedQueue)(queue, scope);
1197
+ const all = runQueue.list();
1198
+ const ready = all.filter((i) => i.state === 'ready');
1199
+ const baseSha = await git.headSha();
1200
+ const runId = (0, core_1.newId)('run');
1201
+ /**
1202
+ * 🔴 AND AN ADDRESSED ONE, BECAUSE ONE SHARED FILE CANNOT BRAKE N RUNS.
1203
+ *
1204
+ * `ControlStore.consume()` writes `resume` into the file it read. With
1205
+ * several executions running, the FIRST to reach a round boundary therefore
1206
+ * cleared the instruction and the rest read `resume` and carried on — while
1207
+ * `looply stop` had already printed "N executions are running; the request
1208
+ * reaches all of them". It reached one.
1209
+ *
1210
+ * `looply stop` now also writes `control.<runId>.json` into each live
1211
+ * execution's own state directory, and this reads its own alongside the
1212
+ * shared one. An addressed instruction can only be consumed by the run it
1213
+ * names, so no run's stop is destroyed by another run obeying.
1214
+ *
1215
+ * ⚠️ THE SHARED FILE IS STILL READ, AND FIRST-EQUAL. It is what the daemon and
1216
+ * the phone write, what a `looply stop` typed while nothing was running
1217
+ * leaves behind for the next run, and what Simple Mode has always used. This
1218
+ * is additive: with one execution the two agree, and with none the addressed
1219
+ * file is never written.
1220
+ *
1221
+ * ⚠️ AND `consume()` CLEARS BOTH, in that order. Clearing only the addressed
1222
+ * one would leave a project-wide `stop` on disk for the next run to obey
1223
+ * without explanation — the permanent-stop failure the runner documents.
1224
+ */
1225
+ const addressedControl = control_file_1.ControlStore.addressed(stateDir, runId, ledger_store_1.nodeLedgerFs);
1226
+ const control = {
1227
+ /**
1228
+ * 🔴 `resume` IS THE CLEARED STATE, NOT AN INSTRUCTION, and the first version
1229
+ * of this treated it as one. `??` falls through only on `null`, and every run
1230
+ * CONSUMES at start — which WRITES `resume` into the addressed file. From
1231
+ * that moment the addressed file always answered, the project's `control.json`
1232
+ * was never read again, and no `looply stop`, no phone Stop and no phone
1233
+ * Pause reached the run. Ever. Including in Simple Mode, where there is only
1234
+ * ever one execution and the whole addressed mechanism is beside the point.
1235
+ *
1236
+ * Measured before the fix: after one start-consume, a project-wide `stop`
1237
+ * read back as `resume` for ever.
1238
+ *
1239
+ * So an addressed file takes precedence only while it carries something to
1240
+ * act on.
1241
+ */
1242
+ read: async () => {
1243
+ const mine = await addressedControl.read();
1244
+ if (mine !== null && mine.kind !== 'resume')
1245
+ return mine;
1246
+ return projectControl.read();
1247
+ },
1248
+ /**
1249
+ * ⚠️ AND THE ADDRESSED FILE IS ONLY WRITTEN WHEN THERE IS SOMETHING IN IT TO
1250
+ * CLEAR. Consuming unconditionally created a `control.<runId>.json` in every
1251
+ * ordinary run's state directory, one per run, for ever — a file Simple Mode
1252
+ * never had. Now only `looply stop` creates one, and only the run it names
1253
+ * clears it.
1254
+ */
1255
+ consume: async () => {
1256
+ const mine = await addressedControl.read();
1257
+ if (mine !== null && mine.kind !== 'resume')
1258
+ await addressedControl.consume();
1259
+ await projectControl.consume();
1260
+ },
1261
+ };
1262
+ /**
1263
+ * 🔴 THE RUN ID IS IN THE BRANCH NAME, and it has to be.
1264
+ *
1265
+ * The stamp is second-resolution, so two runs starting in the same second
1266
+ * produced the IDENTICAL branch — measured: two calls in one process returned
1267
+ * `20260903222307` twice. `createBranch` treats an existing branch as a hard
1268
+ * refusal (git-driver.ts), which is right, so the second run simply died.
1269
+ *
1270
+ * Today that is nearly unreachable, because one lease means one run per
1271
+ * checkout. It stops being unreachable the moment two worktrees run at once:
1272
+ * linked worktrees SHARE the ref namespace, so isolating the state
1273
+ * directories does nothing for this. The stamp stays because it is what makes
1274
+ * a branch list readable by a human; the id is what makes it unique.
1275
+ */
1276
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
1277
+ const branch = `looply/run-${stamp}-${runId.slice(-8)}`;
1278
+ // Computed twice on purpose: the dry run reports the file, and the real run
1279
+ // recomputes below once the phone's settings have been laid over it.
1280
+ const fileMinHours = opts.hours ?? config.run.minHours;
1281
+ if (opts.dryRun) {
1282
+ log(`project : ${config.project.name}`);
1283
+ log(`branch : ${branch} (would be created from ${baseSha.slice(0, 7)})`);
1284
+ log(`budget : ${fileMinHours}h, up to ${config.run.maxRounds} rounds`);
1285
+ log(`queue : ${ready.length} ready`);
1286
+ // Said plainly rather than quietly wrong: the overlay needs the server, and
1287
+ // a dry run deliberately does not open one.
1288
+ log('dry run: settings from your phone are read when the run actually starts.');
1289
+ log(`proof : ${config.proof.enabled ? 'on' : 'OFF — work would be kept on a passing build alone'}`);
1290
+ log(`gates : ${config.gates.map((g) => g.name).join(', ')}`);
1291
+ log('');
1292
+ if (!ready.length) {
1293
+ // A dry run is precisely when an empty queue should be information rather
1294
+ // than an error — you asked what would happen, and this is what would.
1295
+ log(`dry run: nothing would run — ${(0, queue_summary_1.describeNoReady)(all)}`);
1296
+ }
1297
+ else {
1298
+ log(`dry run: would start with "${ready[0].title}".`);
1299
+ }
1300
+ log('dry run: no branch created, no agent called.');
1301
+ return { stopReason: 'command', totals: { rounds: 0, committed: 0, discarded: 0, abandoned: 0 }, elapsedMs: 0 };
1302
+ }
1303
+ /**
1304
+ * The provider preset, resolved BEFORE the lease is taken.
1305
+ *
1306
+ * 🔴 POSITION IS THE POINT. A misspelled provider name is a configuration
1307
+ * mistake, and the run must stop on it having done nothing — not after taking
1308
+ * the working tree's lease, cutting a branch and spending the first model
1309
+ * call. An earlier version of this sat beside the agent construction five
1310
+ * hundred lines below, with a comment claiming it stopped before the lease.
1311
+ * It did not; the lease is taken immediately after this line.
1312
+ *
1313
+ * ⚠️ THE WARNING IS PRINTED, NOT SWALLOWED. Two of the three presets have
1314
+ * never been run by this repository's tests, and somebody starting a night's
1315
+ * work on one should be told before it starts rather than after.
1316
+ */
1317
+ const resolved = (0, providers_1.resolveProvider)({
1318
+ ...(config.provider === undefined ? {} : { provider: config.provider }),
1319
+ spec: config.agent,
1320
+ });
1321
+ if (!('spec' in resolved)) {
1322
+ log(resolved.message);
1323
+ return {
1324
+ stopReason: 'command',
1325
+ totals: { rounds: 0, committed: 0, discarded: 0, abandoned: 0 },
1326
+ elapsedMs: 0,
1327
+ };
1328
+ }
1329
+ if (resolved.warning !== null)
1330
+ log(`provider: ${resolved.warning}`);
1331
+ // One working tree, one mutating runner — taken here, by the process that
1332
+ // will do the mutating, and not by the daemon that may have asked for it. The
1333
+ // daemon spawns a run detached and unrefs it, so a lease holding the daemon's
1334
+ // pid would name a process that is dead while the run it describes is alive,
1335
+ // and every reader of that lease would then act on "dead": start a second
1336
+ // runner, rewrite a live run's queue, tell the phone a healthy run crashed.
1337
+ // Taking it here also covers `looply run` typed by hand, which the daemon
1338
+ // never sees at all.
1339
+ const leases = new run_lease_1.RunLeaseStore(stateDir, run_lease_1.nodeLeaseFs);
1340
+ const baseBranch = await git.currentBranch();
1341
+ // What the last run left behind, judged before anything is taken or written.
1342
+ // Read-only: this decides, the block below acts.
1343
+ /**
1344
+ * 🔴 A QUEUE WRITE IN PROGRESS BLOCKS A RUN, WHICH MAKES THE LOCK TWO-SIDED.
1345
+ *
1346
+ * Scan and verify take a write lock so a run cannot revert their work. That
1347
+ * was only half the arrangement: a run started DURING a verify pass consulted
1348
+ * only the run lease — free, because the pass never takes it — acquired it,
1349
+ * re-opened the queue, and wrote its own snapshot back over every verdict the
1350
+ * pass produced. Measured. The pass reported N confirmed and the items were
1351
+ * all back to `draft`.
1352
+ *
1353
+ * ⚠️ REFUSED, NOT QUEUED. A run that waited would hold the lease while it
1354
+ * waited, which is the deadlock the other direction.
1355
+ */
1356
+ const writing = (0, queue_write_lease_1.queueWriteInProgress)(cwd);
1357
+ if (writing.busy) {
1358
+ /**
1359
+ * ⚠️ A PLAIN THROW, NOT A `RunRefused`. Every `RunRefusalCode` is TERMINAL —
1360
+ * a controller that saw one would stop offering this goal — and "somebody is
1361
+ * writing the queue right now" is the most retryable condition there is. It
1362
+ * ends like a held lease does: exit 1, and the Start is redelivered.
1363
+ *
1364
+ * ⚠️ AND NOTHING IS RELEASED HERE, because nothing is held yet: this sits
1365
+ * before the lease is inspected, let alone acquired.
1366
+ */
1367
+ throw new Error(`cannot start: ${writing.because}`);
1368
+ }
1369
+ const priorState = await leases.inspect();
1370
+ const recovery = (0, recovery_1.classifyRecovery)({
1371
+ lease: priorState.kind === 'dead' ? priorState.lease : null,
1372
+ // The RAW queue, deliberately. Recovery is about what the LAST run left
1373
+ // behind, and that run may have been bound to another goal or to none;
1374
+ // asking a scoped view would hide exactly the stranded work it exists to
1375
+ // find, and this run would then start on top of it.
1376
+ /**
1377
+ * 🔴 BOTH IN-FLIGHT STATES, NOT ONE. `round-runner` writes `verifying`
1378
+ * before it writes `in_progress`, so a run that died between them left an
1379
+ * item nothing could move and every later run in that checkout answered
1380
+ * "nothing to do". See `ItemRepair.unverify` for the measurement.
1381
+ */
1382
+ stranded: queue.list().filter((i) => i.state === 'in_progress' || i.state === 'verifying'),
1383
+ changedPaths: await git.changedPaths(),
1384
+ currentBranch: baseBranch,
1385
+ headSubject: await git.messageOf('HEAD'),
1386
+ // F33: a goal-met Start that moved HEAD and died left it here on purpose.
1387
+ judging: (0, goal_met_target_1.readJudgingMarker)(stateDir),
1388
+ headSha: await git.headSha(),
1389
+ });
1390
+ if (recovery.kind === 'blocked') {
1391
+ // Nothing has been touched: not the queue, not the tree, not a ref. The
1392
+ // person is told what looply found and why it will not guess.
1393
+ log('');
1394
+ log(`recover : ${recovery.reason}`);
1395
+ throw new Error(`cannot start: ${recovery.reason}`);
1396
+ }
1397
+ const lease = await leases.acquire({
1398
+ runId,
1399
+ branch,
1400
+ baseBranch,
1401
+ cliVersion: daemon_cmd_1.CLI_VERSION,
1402
+ ...(opts.commandId === undefined ? {} : { startedByCommandId: opts.commandId }),
1403
+ // Durable, and read by the daemon's launcher after a restart: it is how a
1404
+ // second Start naming a different goal is told apart from a redelivery of
1405
+ // this one.
1406
+ ...(scope === null ? {} : { goalId: scope.goalId }),
1407
+ });
1408
+ /**
1409
+ * CAPACITY, AFTER THE LEASE AND BEFORE ANY WORK.
1410
+ *
1411
+ * The lease answers "is THIS checkout busy" and structurally cannot answer
1412
+ * anything else — leases live one per state directory and cannot see each
1413
+ * other. The registry answers "what else is this PROJECT running", which is a
1414
+ * question a second workspace makes askable for the first time.
1415
+ *
1416
+ * ⚠️ ORDERED AFTER THE LEASE. Taking the slot first would mean a run that then
1417
+ * loses the lease race has to hand a slot back, and a crash between the two
1418
+ * would leak one permanently.
1419
+ *
1420
+ * ⚠️ A PLAIN ERROR, NEVER `RunRefused`. Every `RunRefusalCode` is terminal —
1421
+ * `isContinuableRefusal` returns false for all seven — and a controller that
1422
+ * saw one would send the pursuit to `human_required`. "The machine is busy
1423
+ * right now" is the most retryable condition there is, and it is refused the
1424
+ * same way a held lease is refused: a sentence, and a non-zero exit.
1425
+ */
1426
+ const registry = new workspace_1.RunRegistry(projectStateDirPath);
1427
+ const runRecord = {
1428
+ runId,
1429
+ workspace: cwd,
1430
+ stateDir,
1431
+ branch,
1432
+ baseSha,
1433
+ // The main checkout is not a workspace looply created, and the reaper must
1434
+ // never treat it as one.
1435
+ worktree: (0, node_path_1.resolve)(cwd) !== (0, node_path_1.resolve)((0, trusted_git_1.mainWorktreeRoot)(cwd) ?? cwd),
1436
+ pid: process.pid,
1437
+ host: (0, node_os_1.hostname)(),
1438
+ startedAt: new Date().toISOString(),
1439
+ // M4-2: who asked. `--autonomous` is the launcher's word for a machine start,
1440
+ // and only such a run may ever be asked to stop for a higher-priority goal.
1441
+ startedBy: opts.autonomous === true ? 'machine' : 'person',
1442
+ };
1443
+ /**
1444
+ * CLAIMED, NOT COUNTED. Counting was check-then-act: two runs starting
1445
+ * together both passed it and both proceeded. `claim` takes one of `limit`
1446
+ * exclusively-created slots, so the filesystem decides — see `RunRegistry`.
1447
+ */
1448
+ /**
1449
+ * 🔴 GUARDED, BECAUSE `claim` CAN THROW. Its contract is
1450
+ * `{ ok: false, because }` and it keeps it for every decision — but a
1451
+ * filesystem that refuses a mkdir, a write with an errno outside the
1452
+ * lost-a-race set, or a full disk, all come out as exceptions. This sits forty
1453
+ * lines below `leases.acquire`, so an exception here left the run lease held
1454
+ * by a process that is about to exit, and the next start had to wait for the
1455
+ * pid to be judged dead before it could take over.
1456
+ */
1457
+ let capacity;
1458
+ try {
1459
+ capacity = registry.claim(runRecord, config.run.maxConcurrentRuns ?? 1);
1460
+ }
1461
+ catch (e) {
1462
+ await leases.release(lease.token);
1463
+ throw e;
1464
+ }
1465
+ if (!capacity.ok) {
1466
+ // No `registry.release` here: nothing is registered yet. Releasing would be
1467
+ // harmless — it is idempotent — but it would read as if a slot had been
1468
+ // taken and given back, which is the opposite of what happened.
1469
+ await leases.release(lease.token);
1470
+ throw new Error(capacity.because);
1471
+ }
1472
+ /**
1473
+ * 🔴 GIVING THE HOLD BACK HAS ONE IMPLEMENTATION, AND EVERY EXIT USES IT.
1474
+ *
1475
+ * The first version released the lease, the registry entry and the slot as
1476
+ * three lines copied to four places — and there were ELEVEN other ways out of
1477
+ * the span between here and the try/finally far below, none of which released
1478
+ * anything. Each one left a registry file naming a pid that had already gone.
1479
+ *
1480
+ * That was survivable only by accident: the probe reads a dead pid as dead, so
1481
+ * the slot is reclaimed and the entry reads as an orphan. But it made
1482
+ * `looply runs` report executions that never started, and it put the whole
1483
+ * bound one recycled pid away from a machine that refuses to run at all —
1484
+ * because a leaked record whose pid a later process inherits reads as ALIVE,
1485
+ * forever, and nothing dead ever clears it.
1486
+ *
1487
+ * Idempotent on purpose: the teardown calls it too, and calling it twice must
1488
+ * be as ordinary as calling it once.
1489
+ */
1490
+ let holdReleased = false;
1491
+ /**
1492
+ * The surfaces this run holds, readable from the teardown that is defined
1493
+ * ABOVE the acquisition which fills it.
1494
+ *
1495
+ * A ref rather than a `let`, so there is one place the teardown reads and no
1496
+ * chance of it closing over a stale value.
1497
+ */
1498
+ const heldSurfacesRef = { current: [] };
1499
+ /**
1500
+ * SETTLE THIS RUN'S BUDGET RESERVATION, once the admission below has made one.
1501
+ *
1502
+ * A ref-shaped hook for the same reason `heldSurfacesRef` is: the teardown is
1503
+ * defined above the admission that fills it. Null until there is something to
1504
+ * settle, and `Admission.settle` is itself idempotent, so a teardown that runs
1505
+ * twice charges once.
1506
+ */
1507
+ let settleAdmission = null;
1508
+ const startedAtMs = Date.now();
1509
+ /**
1510
+ * Whether this run has REGISTERED — taken a slot and written its start mark —
1511
+ * and so is a run the daemon may be waiting on. Set once, below, after
1512
+ * `registry.register` succeeds.
1513
+ */
1514
+ let registered = false;
1515
+ /**
1516
+ * The sync loop `attachSync` starts below, once it has — stopped as the LAST
1517
+ * act of `releaseHold`, so every exit that gives the hold back also lets the
1518
+ * process end. See the end of `releaseHold` for why that is not the teardown's
1519
+ * job alone.
1520
+ */
1521
+ let stopSync = null;
1522
+ /** Where the ending below is written: the synced ledger once it exists, the local one before. */
1523
+ let endingSink = ledger;
1524
+ let endingProjectId = config.project.id ?? 'local';
1525
+ /**
1526
+ * M6-16: WHY THIS RUN IS LEAVING BEFORE ITS FIRST ROUND, when it is. Set by
1527
+ * `leaving(...)` at each exit that ends the run between registration and
1528
+ * the runner's start, immediately before that exit's `releaseHold()` — so
1529
+ * the ending written there can say why. Null for a run that started (its
1530
+ * ending is the runner's) and for an exit nobody named.
1531
+ */
1532
+ let endedBefore = null;
1533
+ /** What the compile pass concluded when it queued nothing — appended to the reason, if there is one. */
1534
+ let compileNote = null;
1535
+ const leaving = (because) => {
1536
+ endedBefore = compileNote === null ? because : `${because} (${compileNote})`;
1537
+ };
1538
+ const releaseHold = async () => {
1539
+ if (holdReleased)
1540
+ return;
1541
+ holdReleased = true;
1542
+ /**
1543
+ * 🔴 A RUN THAT REGISTERED AND NEVER STARTED LEAVES ITS OWN ENDING.
1544
+ *
1545
+ * The start mark is written at registration, and the daemon reads it as
1546
+ * "a Run exists" — from then on the controller closes this Start only on a
1547
+ * durable `run.ended` for this run id (`run-ended.ts`). But `run.started`
1548
+ * is emitted after the compile pass and both verify passes, and six exits
1549
+ * sit between registration and it: a superseded goal, an adoption that
1550
+ * fails, a work set with nothing ready (`nothing to do — …`), a decision
1551
+ * that cannot be picked up. The refusals among them write a refusal mark,
1552
+ * which outranks the receipt. The plain errors wrote NOTHING: the run gave
1553
+ * back its lease, its slot and its record and left, and the daemon waited
1554
+ * on a ledger that never mentions it for the life of the grant. MEASURED
1555
+ * end to end with the daemon's own reader in `scripts/coordinator-e2e.js`:
1556
+ * every placed run left by `nothing to do` and no ordinal ever closed.
1557
+ *
1558
+ * So a registered run that leaves without ever having started says so, as
1559
+ * its last act before the record is given back: `aborted`, because a living
1560
+ * process wrote it, with totals that are exactly zero because no round
1561
+ * began. A run that DID start is not touched here — its ending is the
1562
+ * runner's to write, and recovery's when the runner could not.
1563
+ *
1564
+ * ⚠️ NEVER FATAL. A run that could not write its ending still gives
1565
+ * everything back; the cost is the unclosed Start this exists to prevent,
1566
+ * and the line below says so.
1567
+ */
1568
+ if (registered && ledger.currentStatus().runId !== runId) {
1569
+ await writeEndingBeforeFirstRound(endingSink, { runId, projectId: endingProjectId, startedAtMs, because: endedBefore }, log);
1570
+ }
1571
+ /**
1572
+ * 🔴 THE SLOT IS GIVEN BACK BY NAME, NOT BY NUMBER, and the difference is
1573
+ * the whole concurrency bound. `releaseSlot` used to unlink `slot-N.json`
1574
+ * unconditionally; because the lease was released first and `claim` treats
1575
+ * a slot whose holder's lease is gone as free, another run could
1576
+ * legitimately take slot N in between — and this teardown then deleted the
1577
+ * NEW holder's claim. The limit was exceeded, and stayed exceeded, because
1578
+ * the same window reopened at every teardown.
1579
+ *
1580
+ * ⚠️ AND EACH RELEASE IS INDEPENDENT. `holdReleased` is set FIRST so this
1581
+ * stays idempotent under re-entry, which means a throw in one release would
1582
+ * otherwise strand the other two for ever — the teardown's own call would
1583
+ * return at the guard. The lease is last because it is the one whose
1584
+ * absence lets a second run into THIS checkout.
1585
+ */
1586
+ /**
1587
+ * 🔴 SURFACES GO BACK HERE TOO, BECAUSE THE ACQUIRE IS NOT INSIDE THE
1588
+ * `finally` THAT WAS GIVING THEM BACK.
1589
+ *
1590
+ * MEASURED by reading the structure: `acquireSurfaces` runs at ~1782, and
1591
+ * the teardown holding the only `releaseSurface` is the `finally` of a
1592
+ * `try` that does not open until ~2930. Every refusal between those two
1593
+ * points — a stale base, an unreadable safety history, a queue that will
1594
+ * not open, an approval that is outstanding, a compile that fails, a
1595
+ * `RunRefused` of any kind — returned or threw with the lease still held.
1596
+ * A surface leaked that way is held until its TTL lapses: six hours of a
1597
+ * project refusing to schedule itself, for a directory nobody is in.
1598
+ *
1599
+ * ⚠️ AND IT IS IDEMPOTENT, so the `finally` downstream may still call it.
1600
+ * `releaseSurface` marks rather than deletes and matches on runId, so a
1601
+ * second release is a no-op rather than a second writer.
1602
+ *
1603
+ * ⚠️ FIRST IN THE TEARDOWN, because a surface is the only thing here that
1604
+ * blocks OTHER PROJECTS' runs rather than only this checkout's.
1605
+ */
1606
+ /**
1607
+ * 🔴 THE BUDGET IS SETTLED FIRST, because a reservation left open holds an
1608
+ * ESTIMATE against the window until its hold expires — and on a machine
1609
+ * running near its ceiling that is the difference between the next goal
1610
+ * starting and waiting five minutes for nothing.
1611
+ */
1612
+ try {
1613
+ settleAdmission?.();
1614
+ }
1615
+ catch {
1616
+ /* the hold expires by itself; a settlement that will not write is not fatal */
1617
+ }
1618
+ for (const fence of heldSurfacesRef.current) {
1619
+ try {
1620
+ (0, surface_lease_1.releaseSurface)(cwd, fence, runId);
1621
+ }
1622
+ catch {
1623
+ /* a filesystem that said no. The TTL is the backstop. */
1624
+ }
1625
+ }
1626
+ try {
1627
+ registry.releaseSlot(capacity.slot, runId);
1628
+ }
1629
+ catch {
1630
+ /* another run's slot, or a filesystem that said no. Never fatal here. */
1631
+ }
1632
+ try {
1633
+ registry.release(runId);
1634
+ }
1635
+ catch {
1636
+ /* the record is bookkeeping; the lease below is what matters. */
1637
+ }
1638
+ await leases.release(lease.token);
1639
+ // Named for a runId that will never exist again, so nothing can ever read
1640
+ // it. Left behind, `looply stop` would put one in an ordinary checkout's
1641
+ // state directory every time a person pressed stop, for ever.
1642
+ (0, workspace_1.clearAddressedControl)(stateDir, runId);
1643
+ /**
1644
+ * 🔴 AND THE SYNC LOOP STOPS, LAST — IT IS THE ONE THING HERE THAT KEEPS
1645
+ * THE PROCESS ALIVE.
1646
+ *
1647
+ * `attachSync` starts a loop that polls on a timer until `stop()`. The
1648
+ * teardown's `finally` and two early exits stopped it; the eight plain
1649
+ * refusals between `attachSync` and the teardown's `try` gave back the
1650
+ * lease, the slot, the record and the surfaces here, wrote their ending, and
1651
+ * threw — and the loop went on polling for ever, so the process never
1652
+ * exited. MEASURED live (LIVE-2): a run refused by the envelope wrote
1653
+ * `run.ended` and was still alive minutes later with one live timer,
1654
+ * `sleep ← Daemon.run`, and nothing else.
1655
+ *
1656
+ * ⚠️ AFTER the ending above, because stopping is what flushes it. `stop()`
1657
+ * never rethrows and is idempotent, so the teardown's own stop after this
1658
+ * one costs nothing.
1659
+ */
1660
+ if (stopSync !== null)
1661
+ await stopSync();
1662
+ };
1663
+ /**
1664
+ * 🔴 ANYTHING THAT THROWS OUT OF A CALL MUST STILL GIVE BACK WHAT THIS RUN
1665
+ * IS HOLDING — AND SEVEN CALLS BETWEEN HERE AND THE ROUND LOOP DID NOT.
1666
+ *
1667
+ * Every `throw` STATEMENT in this stretch is preceded by `await releaseHold()`,
1668
+ * and `run-hold-release.spec.ts` pins that. What neither the discipline nor
1669
+ * the pin can see is an exception propagating out of a CALL: there is no
1670
+ * `throw` token at the call site, so the guard was never written and the pin
1671
+ * has nothing to match. Its own docblock says as much.
1672
+ *
1673
+ * What that costs, at the last of them (`exemptionStore.open()`): the run
1674
+ * lease, the capacity slot, the registry record, every surface this run holds,
1675
+ * AND AN OPEN BUDGET RESERVATION — which goes on charging its estimate against
1676
+ * the window until the hold expires — plus the creation marker, whose absence
1677
+ * leaves the checkout unreapable once the pid is recycled. At the earliest of
1678
+ * them the queue is simply corrupt, which `QueueStore.open` throws on BY
1679
+ * DESIGN, so this is not a hypothetical errno.
1680
+ *
1681
+ * ⚠️ IT RETHROWS, ALWAYS. This gives back a hold; it does not decide
1682
+ * anything, and swallowing here would turn a broken machine into a silent one.
1683
+ * `releaseHold` is idempotent, so a call that already released costs nothing.
1684
+ */
1685
+ /**
1686
+ * Throws when this run was constructed to fail at `site`. See `RunFaultSite`.
1687
+ * Absent in every production caller, so this is a no-op everywhere but a test.
1688
+ */
1689
+ const faulting = (site) => {
1690
+ if (opts.faultAt === site) {
1691
+ throw new Error(`injected fault at ${site} (test-only; see RunFaultSite)`);
1692
+ }
1693
+ };
1694
+ const holding = async (fn) => {
1695
+ try {
1696
+ return await fn();
1697
+ }
1698
+ catch (e) {
1699
+ await releaseHold();
1700
+ throw e;
1701
+ }
1702
+ };
1703
+ /**
1704
+ * The readable record of what holds the slot — what `looply runs` and the
1705
+ * reaper work from.
1706
+ *
1707
+ * 🔴 DEFINED AFTER `releaseHold`, AND GUARDED, because it can throw. It was
1708
+ * three lines ABOVE the definition, so an EEXIST on a repeated runId, a
1709
+ * filesystem refusal, or a runId this cannot address left the lease AND the
1710
+ * slot held with nothing able to give them back — and the source pin below
1711
+ * could not see it, because the pin looks for `throw` STATEMENTS and this is
1712
+ * an exception out of a call.
1713
+ */
1714
+ /**
1715
+ * 🔴 PROJECT-SCOPED, BECAUSE THESE ARE THE DAEMON↔RUN WIRE — AND EXECUTION
1716
+ * SCOPING THEM ESCALATED EVERY COORDINATED GOAL TO A HUMAN.
1717
+ *
1718
+ * A LEASE is about a CHECKOUT: one run per working tree, so it belongs beside
1719
+ * the tree. A START MARK is about a REQUEST — durable proof that the Start the
1720
+ * daemon asked for produced a Run — and the daemon is its only reader.
1721
+ *
1722
+ * MEASURED end to end: a coordinator-placed run wrote `run-starts.json` into
1723
+ * its WORKSPACE, and the daemon read it from its own checkout. `provenStart`
1724
+ * therefore answered null for every placed run, the launcher never reported
1725
+ * `started`, and `autonomy-controller` concluded
1726
+ *
1727
+ * Start 1 was attempted 3 times and never began
1728
+ *
1729
+ * and escalated a perfectly healthy goal to `human_required` — three cadences
1730
+ * into every night, for every goal the coordinator placed.
1731
+ *
1732
+ * ⚠️ INVISIBLE IN A SINGLE CHECKOUT, which is why nothing caught it: there the
1733
+ * project and execution state directories ARE the same path, so writer and
1734
+ * reader agree by accident. It only appears once a run is placed somewhere
1735
+ * else, which is the entire point of a coordinator.
1736
+ */
1737
+ const startMarks = new run_start_marks_1.RunStartMarks(projectStateDirPath, ledger_store_1.nodeLedgerFs, log);
1738
+ // READ BEFORE WRITING. Without this the store holds an empty list, and
1739
+ // recording this run rewrites the whole file with one entry — so the fifty
1740
+ // the cap allows for have never actually accumulated, and any question about
1741
+ // an earlier request answers "no run". Harmless while the only reader asked
1742
+ // about the newest request; not harmless once anything asks retrospectively.
1743
+ if (opts.commandId !== undefined)
1744
+ await holding(() => {
1745
+ faulting('startMarks.load');
1746
+ return startMarks.load();
1747
+ });
1748
+ try {
1749
+ registry.register(runRecord);
1750
+ }
1751
+ catch (e) {
1752
+ await releaseHold();
1753
+ throw e;
1754
+ }
1755
+ registered = true;
1756
+ /**
1757
+ * 🔴 THE START RECEIPT IS WRITTEN HERE, BECAUSE HERE IS WHERE IT BECOMES
1758
+ * TRUE — AND WRITING IT AT `run.started` ESCALATED PAID RUNS TO A HUMAN.
1759
+ *
1760
+ * `markingStartsOn` recorded the receipt when the ledger first appended
1761
+ * `run.started`, which `RoundRunner` emits after the compile pass, both
1762
+ * verify passes and the adaptive evaluation. So a Start could hold the lease,
1763
+ * take a capacity slot, reserve budget, spend a compile and two verify passes
1764
+ * — tens of minutes and real money — leave by any of the six refusals between
1765
+ * here and there, and leave NO EVIDENCE THAT IT EVER RAN. The launcher then
1766
+ * answers `failed` for that request for ever after, and three such Starts
1767
+ * produce
1768
+ *
1769
+ * Start 1 was attempted 3 times and never began
1770
+ *
1771
+ * on a goal that had begun three times and been charged for it.
1772
+ *
1773
+ * MEASURED on the real command, single checkout, idle machine, trivial queue:
1774
+ * the run appeared in the registry at 524 ms and the receipt appeared at
1775
+ * 769 ms. Anything that ends a run inside that window — a kill, a crash, a
1776
+ * machine going to sleep — produces the escalation above. With a real queue
1777
+ * the window is the whole compile-and-verify pass, which is minutes.
1778
+ *
1779
+ * Registration is the honest instant: the record is on disk, the lease and
1780
+ * the slot are held, and `registry.active()` already counts this run. Nothing
1781
+ * downstream can make it untrue that a Run existed.
1782
+ *
1783
+ * ⚠️ IT DOES NOT MEAN THE START SUCCEEDED, and the launcher no longer reads
1784
+ * it that way: a refusal mark for the same request outranks this receipt, so
1785
+ * the six terminal refusals downstream still speak for the goal.
1786
+ *
1787
+ * ⚠️ NEVER FATAL, and never for a hand-typed run. A run genuinely under way
1788
+ * must not be killed because the daemon's evidence file would not write; the
1789
+ * cost is an unacked command, which the redelivery window already bounds.
1790
+ */
1791
+ if (opts.commandId !== undefined) {
1792
+ try {
1793
+ await startMarks.record({
1794
+ commandId: opts.commandId,
1795
+ runId,
1796
+ at: new Date().toISOString(),
1797
+ // The checkout this run is executing in. The daemon reads THIS to find
1798
+ // the run's ledger after it ends — the registry record is given back
1799
+ // on the way out, this is not.
1800
+ workspace: cwd,
1801
+ });
1802
+ }
1803
+ catch (e) {
1804
+ log(`start : could not record that this run began (${e.message}) — the phone may ask again`);
1805
+ }
1806
+ }
1807
+ /**
1808
+ * ⚠️ THE CREATION MARKER IS NOT CLEARED HERE — it is cleared as the LAST thing
1809
+ * this run does, at the very end of the teardown below.
1810
+ *
1811
+ * 🔴 THIS DOCBLOCK USED TO SAY IT WAS NEVER CLEARED AT ALL, twenty lines from
1812
+ * the call that clears it. Both extremes were shipped in turn — cleared here,
1813
+ * which reopened the window it exists to close; then never cleared, which made
1814
+ * a finished checkout permanently unreapable once its pid was recycled — and
1815
+ * the comment was left describing the middle one.
1816
+ *
1817
+ * The marker names THIS PROCESS, so it covers the checkout for the process's
1818
+ * whole life, including the teardown window where `releaseHold` has already
1819
+ * given back the lease and the record.
1820
+ */
1821
+ /**
1822
+ * READ THE QUEUE AGAIN, now that nothing else may write it.
1823
+ *
1824
+ * `queue.open()` ran a long way above this — before three git subprocesses
1825
+ * and before the wait for the lease — and the store answers `list()` from the
1826
+ * map it filled then. It never goes back to disk. So without this line every
1827
+ * decision below is taken against a snapshot of how things were BEFORE this
1828
+ * run had exclusive use of the tree: whether this goal version already has a
1829
+ * plan, what recovery should put back, what a round may pick.
1830
+ *
1831
+ * A stale snapshot here is not merely a stale opinion, which is what makes it
1832
+ * worth a second read rather than a comment. `insertAll` commits
1833
+ * `[...this.items.values(), ...inserted]` — the whole map, not a delta — so
1834
+ * whatever this process failed to see is not just missed, it is written back
1835
+ * over. A `release-goal-items` that took the lease first and let a goal's work
1836
+ * go would be silently undone by the next thing this run wrote.
1837
+ *
1838
+ * The lease is the only mutual exclusion in the system. Reading outside it and
1839
+ * writing inside it is the one arrangement that cannot work.
1840
+ */
1841
+ await holding(() => {
1842
+ faulting('queue.open');
1843
+ return queue.open();
1844
+ });
1845
+ /**
1846
+ * BEFORE anything is written, refuse a queue that holds a LATER plan.
1847
+ *
1848
+ * This check used to stand further down, next to the compile decision — and
1849
+ * two things that write the queue stood in front of it: adopting the items a
1850
+ * Start named, and putting stranded work back. So the one case that is
1851
+ * supposed to touch nothing touched two things first, and a Start naming
1852
+ * items would take ownership of them on behalf of an authority it was about
1853
+ * to declare stale.
1854
+ *
1855
+ * Versions only go up, so a plan compiled for a later version is durable
1856
+ * evidence that this run's authority is out of date. The honest response to
1857
+ * "your instructions have been overtaken" is to do nothing at all, and doing
1858
+ * nothing has to include the repairs: recovery is skipped too, deliberately,
1859
+ * because a queue that has been rewritten underneath this process is not one
1860
+ * to tidy on the strength of a Start that should never have arrived. The next
1861
+ * run, under an authority that agrees with the queue, recovers it.
1862
+ */
1863
+ if (scope !== null) {
1864
+ const contradicting = (0, goal_ownership_1.splitGoalWork)(queue.list(), scope).newerPlans;
1865
+ if (contradicting.length) {
1866
+ const versions = [...new Set(contradicting.map((i) => i.compiledForGoalVersion))].join(', ');
1867
+ const superseded = `this machine holds work compiled for a LATER version of this goal (${versions}, and this Start says ` +
1868
+ `${binding.kind === 'bound' ? binding.goal.version : '?'}) — refusing to run against evidence that ` +
1869
+ 'the goal has already moved on';
1870
+ leaving(superseded);
1871
+ await releaseHold();
1872
+ throw new RunRefused(superseded,
1873
+ // Durable evidence that the goal is ahead of this Start. Not a guess:
1874
+ // a plan compiled for a later version cannot exist under a correct
1875
+ // history, so the goal has moved and this Start is behind it.
1876
+ 'goal_superseded');
1877
+ }
1878
+ }
1879
+ // Under the lease, because it writes the queue. One durable write for the
1880
+ // whole set: a crash partway through would otherwise leave the goal owning
1881
+ // some of what the phone named and nothing recording which.
1882
+ if (scope !== null && opts.itemIds?.length) {
1883
+ const goalId = scope.goalId;
1884
+ const itemIds = opts.itemIds;
1885
+ const adopted = await holding(() => {
1886
+ faulting('adoptGoalItems');
1887
+ return (0, goal_ownership_1.adoptGoalItems)(queue, goalId, itemIds);
1888
+ });
1889
+ if (!adopted.ok) {
1890
+ leaving(`cannot start this goal — ${adopted.problem}`);
1891
+ await releaseHold();
1892
+ // Terminal: the named items will not become adoptable by being asked for
1893
+ // again in three seconds.
1894
+ throw new RunRefused(`cannot start this goal — ${adopted.problem}`, 'run_refused');
1895
+ }
1896
+ log(`goal : ${adopted.adopted} item(s) taken on` +
1897
+ (adopted.alreadyOwned ? `, ${adopted.alreadyOwned} already this goal's` : ''));
1898
+ }
1899
+ const dispatcher = new notify_channel_1.ChannelDispatcher({
1900
+ exec: notify_channel_1.nodeNotifyExec,
1901
+ fileExists: (p) => (0, node_fs_1.existsSync)(p),
1902
+ log,
1903
+ homeDir: (0, node_os_1.homedir)(),
1904
+ }, config.notify.channel);
1905
+ dispatcher.preflight();
1906
+ // Two audiences, two rules. The cadence notifier decides whether to
1907
+ // interrupt somebody who is not here, and obeys notify.every_min. The
1908
+ // terminal one narrates to somebody who is, and has no rate limit: a run
1909
+ // that printed three header lines and then nothing for hours is
1910
+ // indistinguishable from a hung one, which is the failure mode this whole
1911
+ // product is written against.
1912
+ const terminal = new progress_view_1.TerminalNotifier(log);
1913
+ // implement() runs exactly once per round, so this is the round number.
1914
+ let roundNo = 0;
1915
+ const cadence = new notify_cadence_1.CadenceNotifier(config.notify.everyMin, { send: (text) => dispatcher.send(text, config.notify.to) }, () => Date.now());
1916
+ const notifier = (0, progress_view_1.fanOut)(terminal, cadence);
1917
+ // Every agent this run starts is registered here, so the run can stop them
1918
+ // on the way out rather than leaving one editing the repository with no
1919
+ // parent. Covers an ordinary exit, a throw, a Ctrl-C and a `looply stop`;
1920
+ // a SIGKILL of this process cannot be covered from inside it.
1921
+ const children = new owned_children_1.OwnedChildren();
1922
+ const signals = new signals_1.SignalWatcher({
1923
+ proc: (0, signals_1.nodeSignalSource)(),
1924
+ // First Ctrl-C: the runner finishes the round in flight, so the tree is
1925
+ // never left half-written. Said out loud because otherwise a run that
1926
+ // appears to ignore Ctrl-C invites a second, harder one.
1927
+ onGraceful: () => {
1928
+ log('');
1929
+ log('run : stopping after this round. Press Ctrl-C again to stop now.');
1930
+ },
1931
+ // Second: the round will not be finished, so kill what this run started
1932
+ // rather than leaving an agent editing the tree with no parent. Both of
1933
+ // these callbacks existed and neither was ever wired.
1934
+ onForce: () => {
1935
+ const killed = children.killAll();
1936
+ log(`run : stopping now — ${killed} agent process(es) stopped.`);
1937
+ },
1938
+ });
1939
+ signals.install();
1940
+ // If this machine is paired, every event the run writes also goes to the
1941
+ // server. Without it a run was correct, complete and invisible — the phone
1942
+ // showed a project that never moved. Unpaired is a normal state, not an
1943
+ // error: looply works on its own and the app is an addition.
1944
+ const sync = await (0, sync_1.attachSync)(cwd, config, log);
1945
+ stopSync = sync === null ? null : () => sync.stop();
1946
+ const synced = sync ? new syncing_ledger_1.SyncingLedger(ledger, sync.daemon, (0, syncing_ledger_1.logSinkErrorOnce)(log)) : ledger;
1947
+ endingSink = synced;
1948
+ /**
1949
+ * Say, durably, that the command which asked for this run got one.
1950
+ *
1951
+ * Wrapped around the ledger rather than hooked into RoundRunner, because the
1952
+ * moment that matters is precisely when `run.started` is appended — and the
1953
+ * runner already does that, for its own reasons, knowing nothing about
1954
+ * commands. This notices; the runner is unchanged.
1955
+ *
1956
+ * Only for a run the daemon asked for. A hand-typed `looply run` has no
1957
+ * command to prove anything to.
1958
+ */
1959
+ /**
1960
+ * ⚠️ STILL HERE, AND NOW ALMOST ALWAYS A NO-OP. The receipt is written at
1961
+ * registration above; `RunStartMarks.record` returns early when the command
1962
+ * already has one, so this writes nothing on any path that registered.
1963
+ *
1964
+ * It is kept because it costs nothing and it is the only writer that would
1965
+ * still fire if a future path reached `run.started` without registering —
1966
+ * which is exactly the kind of silent gap this whole receipt exists to close.
1967
+ */
1968
+ const onMarkError = (e) => log(`start : could not record that this run began (${e.message}) — the phone may ask again`);
1969
+ // `cwd` is carried so a mark written here names the checkout, exactly as the
1970
+ // one written at registration does.
1971
+ const ledgerForRun = opts.commandId === undefined ? synced : (0, run_start_marks_1.markingStartsOn)(synced, startMarks, opts.commandId, onMarkError, cwd);
1972
+ /**
1973
+ * What you set on your phone, laid over the file.
1974
+ *
1975
+ * Only settings somebody actually chose are applied — the server answers with
1976
+ * the whole registry, and its defaults are not this project's. Failure is not
1977
+ * fatal: a run that cannot reach the server still runs, on the file alone,
1978
+ * which is the same rule every other server call here follows.
1979
+ */
1980
+ if (sync) {
1981
+ try {
1982
+ const { settings } = await sync.client.projectSettings(sync.projectId);
1983
+ const overlay = (0, settings_overlay_1.applySettings)(config, settings);
1984
+ config = overlay.config;
1985
+ for (const line of (0, settings_overlay_1.describeOverrides)(overlay))
1986
+ log(line);
1987
+ }
1988
+ catch (e) {
1989
+ log(`settings: could not read your phone's settings (${e.message}) — using ${'.looply/config.yaml'} alone`);
1990
+ /**
1991
+ * ...EXCEPT FOR PUBLISHING, WHICH NARROWS INSTEAD.
1992
+ *
1993
+ * Falling back to the file is right for every other setting: a run that
1994
+ * cannot reach the server should still run, and `max_rounds` from a file
1995
+ * somebody committed is a perfectly good answer.
1996
+ *
1997
+ * It is the wrong answer for exactly one key. The phone is where
1998
+ * publishing gets turned OFF, and "I could not ask" and "nobody has
1999
+ * turned it off" are the same silence and opposite facts — the argument
2000
+ * `approval-gate-read` already makes for `unknown`. A file saying
2001
+ * `on-approval` and a person who set `never` from the sofa an hour ago
2002
+ * would otherwise resolve, here, in favour of the file.
2003
+ *
2004
+ * A TOTAL OUTAGE COULD NOT REACH A PUSH ANYWAY: the governed path asks the
2005
+ * server to read the approval, to claim it and to mint an execution, and
2006
+ * refuses if any of them cannot be reached. What this closes is the
2007
+ * PARTIAL case — settings failing while the approval routes answer — which
2008
+ * is narrow, real, and the kind of gap only found by checking a shipped
2009
+ * thing against what was promised for it.
2010
+ */
2011
+ if ((0, config_1.mayPublish)(config.publish.policy)) {
2012
+ log('settings: publishing is off for this run — your phone could not be asked');
2013
+ config = { ...config, publish: { policy: 'never' } };
2014
+ }
2015
+ }
2016
+ }
2017
+ // Now that the phone has had its say. `--hours` still wins: it is the most
2018
+ // deliberate thing in the chain, typed on the machine that is about to run.
2019
+ const minHours = opts.hours ?? config.run.minHours;
2020
+ /**
2021
+ * Built here, AFTER the sync, because only the sync knows the project id.
2022
+ *
2023
+ * This was constructed before `attachSync` ran and read `config.project.id`,
2024
+ * which no config carries — `looply init` writes `project.name` and nothing
2025
+ * else — so every event a run wrote was stamped `projectId: 'local'`. The
2026
+ * upload then worked perfectly: the daemon shipped the batch, the server
2027
+ * accepted the request, and ingest dropped every event on
2028
+ * `.filter(e => e.projectId === projectId)` because the id in the envelope
2029
+ * was not the id in the URL. Nothing failed, nothing was logged, and the
2030
+ * phone showed a project that never moved — the precise failure SyncingLedger
2031
+ * exists to prevent, reintroduced one line above it.
2032
+ *
2033
+ * `verifyDrafts` and the scan already resolved it this way. This was the odd
2034
+ * one out, which is what let it survive.
2035
+ *
2036
+ * Bound separately from the identity because recovery below is announced as
2037
+ * events too, and an event needs the same id.
2038
+ */
2039
+ const projectId = sync?.projectId ?? config.project.id ?? 'local';
2040
+ endingProjectId = projectId;
2041
+ const identity = { runId, projectId, lane: 'feature', branch };
2042
+ // Applied here rather than where it was decided, because the repair is
2043
+ // announced as events and an event needs the project id the server
2044
+ // registered — the same reason the run identity is built here.
2045
+ //
2046
+ // Event-first on purpose: the machine does not reach into the server's tables
2047
+ // to fix them. It says what happened on the ledger it already owns, and every
2048
+ // projection converges from that. One execution history, many mirrors.
2049
+ if (recovery.kind === 'repairable' && (recovery.items.length || recovery.closeRun)) {
2050
+ await holding(() => {
2051
+ faulting('applyRecovery');
2052
+ return (0, recovery_io_1.applyRecovery)(recovery, {
2053
+ queue,
2054
+ ledger: ledgerForRun,
2055
+ // M5-5: the daemon's sweep may have closed this same dead run a moment
2056
+ // ago — measured: one second before, and the ending was written twice.
2057
+ // The ledger is read at write time; a second ending is never written.
2058
+ alreadyEnded: async (id) => {
2059
+ const ended = await (0, run_ended_1.readRunEnded)(stateDir, ledger_store_1.nodeLedgerFs, id);
2060
+ return ended.ok ? ended.ended : null;
2061
+ },
2062
+ projectId,
2063
+ now: () => Date.now(),
2064
+ log,
2065
+ });
2066
+ });
2067
+ }
2068
+ /**
2069
+ * WHAT A PERSON ALREADY DECIDED, WHEN THIS RUN IS PICKING IT UP.
2070
+ *
2071
+ * The daemon carried one id here. Everything the model will be told is read
2072
+ * now, from the row that person's decision was written to — the same place,
2073
+ * and the same wording, they saw before they answered.
2074
+ *
2075
+ * READ BEFORE THE FIRST ROUND, AND FATAL IF IT CANNOT BE. A run resumed on a
2076
+ * refusal it could not read is a run that plans as though nothing was
2077
+ * refused, and the likeliest thing it plans is asking for the same action
2078
+ * again — which stops it at the same boundary, having spent a Start.
2079
+ *
2080
+ * A PLAIN ERROR, DELIBERATELY NOT A REFUSAL. Every `RunRefusalCode` is
2081
+ * terminal: throwing one here would end a person's whole grant because a
2082
+ * single HTTP read failed. This exits retryable, and the next Start reads it
2083
+ * again.
2084
+ */
2085
+ let resolution;
2086
+ if (opts.resumedFromApprovalId !== undefined) {
2087
+ if (binding.kind !== 'bound' || goalServer === null) {
2088
+ leaving('a run cannot pick up a decision without the goal it was made for');
2089
+ await releaseHold();
2090
+ throw new Error('a run cannot pick up a decision without the goal it was made for');
2091
+ }
2092
+ const got = await (0, approval_resolution_1.readApprovalResolution)(goalServer.resolution, opts.resumedFromApprovalId, {
2093
+ projectId,
2094
+ goalId: binding.goal.id,
2095
+ goalVersion: binding.goal.version,
2096
+ now: Date.now(),
2097
+ });
2098
+ if (!got.ok) {
2099
+ leaving(`could not pick up that decision — ${got.because}`);
2100
+ await releaseHold();
2101
+ throw new Error(`could not pick up that decision — ${got.because}`);
2102
+ }
2103
+ resolution = got.resolution;
2104
+ log(`action : picking up where you said ${resolution.outcome} — ${resolution.display}`);
2105
+ }
2106
+ /**
2107
+ * THE ONE THING A PERSON ALREADY SAID YES TO, DONE BEFORE ANYTHING IS ASKED.
2108
+ *
2109
+ * Before the agent exists, before a queue is compiled, before a single round.
2110
+ * If somebody approved publishing an artifact, opening this run by asking a
2111
+ * model what it would like to do would be the wrong first act — and worse, it
2112
+ * would let a model's next idea arrive before the thing already authorised.
2113
+ *
2114
+ * NOTHING HERE DECIDES ANYTHING. The whole step is one call into
2115
+ * `governed-run`, which owns every word about claims and execution ids
2116
+ * precisely so this file does not — a guard written at 7B4-B asserts that,
2117
+ * and it is still passing.
2118
+ *
2119
+ * A REFUSAL IS A PLAIN ERROR, not a `RunRefusalCode`. Every refusal code is
2120
+ * terminal, and ending a person's grant because a governed step could not
2121
+ * begin would answer a temporary problem with a permanent one.
2122
+ */
2123
+ /**
2124
+ * WHY THIS LOOP WILL STOP, ARMED ONCE AND READ BY A RUNNER THAT CANNOT TELL WHY.
2125
+ *
2126
+ * Declared HERE rather than beside the approval machinery it used to sit
2127
+ * with, because the governed prehook below can arm it before a single round
2128
+ * exists. `RoundRunner` reads this at the top of its loop — before the pick,
2129
+ * not only after a model turn — and a comment there says that nothing arms it
2130
+ * that way today and the fence is for the day something does. This is that
2131
+ * day: an ambiguous governed result ends the run having invoked no model at
2132
+ * all, which is exactly what the fence was written for.
2133
+ */
2134
+ let halt = null;
2135
+ let governed;
2136
+ if (opts.governedApprovalId !== undefined) {
2137
+ if (binding.kind !== 'bound' || goalServer === null) {
2138
+ leaving('a run cannot carry out an approved action without the goal it was for');
2139
+ await releaseHold();
2140
+ throw new Error('a run cannot carry out an approved action without the goal it was for');
2141
+ }
2142
+ const outcome = await (0, governed_run_1.runGovernedAction)({
2143
+ server: goalServer.governed,
2144
+ /**
2145
+ * EVERY EXECUTOR LOOPLY HAS, and the policy decides which of them can
2146
+ * ever be reached rather than which are registered here.
2147
+ *
2148
+ * Registering the push executor unconditionally is deliberate: a broker
2149
+ * assembled differently per policy would be a second place the policy
2150
+ * lives, and the two would eventually disagree. The gate is in one
2151
+ * place — before the claim — and this list is simply what Looply can
2152
+ * do.
2153
+ */
2154
+ broker: (0, governed_broker_1.createGovernedBroker)([
2155
+ (0, governed_broker_1.fixturePublishExecutor)({ fs: start_attempts_1.nodeAttemptFs }),
2156
+ (0, git_push_executor_1.gitPushExecutor)(new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(cwd))),
2157
+ /**
2158
+ * 🔴 ONE APPROVED ORDERING CONSTRAINT, CARRIED OUT. Registered
2159
+ * unconditionally beside the others, for the reason given above: a
2160
+ * broker assembled differently per policy would be a second place the
2161
+ * policy lives.
2162
+ *
2163
+ * ⚠️ THE CAPABILITY IS `goalServer.edges`, NOT THE CLIENT, and the
2164
+ * first draft of this reached for the client and would not compile
2165
+ * — `openGoalServer` keeps it inside itself on purpose, because
2166
+ * handing one out puts deciding, listing and claiming within reach of
2167
+ * the same call site.
2168
+ */
2169
+ (0, goal_depends_executor_1.goalDependsExecutor)(projectId, goalServer.edges),
2170
+ ]),
2171
+ // 🔴 PROJECT-SCOPED: the daemon RESERVES under `projectStateDir` at the
2172
+ // Start boundary, and this is the reader that re-derives the claim key
2173
+ // from that reservation. Read from the execution state, a placed run
2174
+ // found no reservation for the approval it was started to carry out.
2175
+ reservations: new governed_approvals_1.GovernedApprovals(projectStateDirPath, start_attempts_1.nodeAttemptFs),
2176
+ log: (line) => log(`action : ${line}`),
2177
+ }, {
2178
+ projectId,
2179
+ goalId: binding.goal.id,
2180
+ goalVersion: binding.goal.version,
2181
+ runId,
2182
+ // From the trusted daemon-to-child wire, never from anything a model
2183
+ // or a person typed alongside the approval id.
2184
+ ...(opts.commandId === undefined ? { commandId: undefined } : { commandId: opts.commandId }),
2185
+ approvalId: opts.governedApprovalId,
2186
+ publishPolicy: config.publish.policy,
2187
+ });
2188
+ if (outcome.kind === 'refused') {
2189
+ leaving(`that action was not carried out — ${outcome.because}`);
2190
+ await releaseHold();
2191
+ throw new Error(`that action was not carried out — ${outcome.because}`);
2192
+ }
2193
+ if (outcome.kind === 'stop') {
2194
+ // The attempt was made and its result is not something to plan around.
2195
+ // The pursuit stays alive and held; a person decides what happens next.
2196
+ halt = { stopReason: 'governed-action-ambiguous', because: outcome.because };
2197
+ log(`action : ${outcome.because}`);
2198
+ }
2199
+ else if (outcome.kind === 'done') {
2200
+ governed = outcome.result;
2201
+ /**
2202
+ * F34 (group B walk #5): AN APPLIED EDGE MOVED THE GOAL TO A NEW VERSION, and everything this
2203
+ * run resolved — its binding, its plan, its grant — is about the old one. Working on meant a
2204
+ * wasted compile at best, and at worst work published under a claim the lane then calls stale.
2205
+ * Retryable, not terminal: the goal's next Start works the new version.
2206
+ */
2207
+ if (governed.actionType === 'goal.depends' && governed.outcome === 'succeeded') {
2208
+ leaving(`the goal gained a dependency (${governed.display}) — its next Start works the new version`);
2209
+ await releaseHold();
2210
+ throw new Error(`the goal gained a dependency — ${governed.display}; its next Start works the new version`);
2211
+ }
2212
+ }
2213
+ }
2214
+ /**
2215
+ * A RUN THAT IS NOT THE ANSWER TO THE QUESTION DOES NOT GET TO REASON.
2216
+ *
2217
+ * The completion fence at the end of this file already refuses to call a goal
2218
+ * done while somebody is owed an answer, and that was enough while the worst
2219
+ * a run could do was waste itself. It is not enough now.
2220
+ *
2221
+ * Reached only when this run carries NEITHER a decision to pick up nor an
2222
+ * action to carry out. In that case any outstanding approval work belongs to
2223
+ * some other Start, and there is nothing this run can usefully do with a
2224
+ * model: it would reason for hours against a pursuit that is waiting on a
2225
+ * person, and the likeliest thing it proposes is the action already waiting.
2226
+ *
2227
+ * BEFORE THE AGENT EXISTS, which is the whole point of its position. Meeting
2228
+ * this at the fence instead would mean the model calls were already paid for.
2229
+ *
2230
+ * A PLAIN ERROR, like the fence: every `RunRefusalCode` is terminal, and
2231
+ * ending a person's grant because they have not answered yet would answer a
2232
+ * question they are still thinking about.
2233
+ */
2234
+ if (opts.resumedFromApprovalId === undefined &&
2235
+ opts.governedApprovalId === undefined &&
2236
+ binding.kind === 'bound' &&
2237
+ goalServer !== null) {
2238
+ const outstanding = await (0, approval_gate_read_1.readApprovalGate)(goalServer.gate, {
2239
+ projectId,
2240
+ goalId: binding.goal.id,
2241
+ goalVersion: binding.goal.version,
2242
+ now: Date.now(),
2243
+ consumed: (ids) => resumes.consumed(ids),
2244
+ });
2245
+ const waiting = (0, core_1.approvalHold)(outstanding);
2246
+ if (waiting !== null) {
2247
+ log(`action : not starting work — ${waiting}`);
2248
+ leaving(`this goal is waiting on you — ${waiting}`);
2249
+ await releaseHold();
2250
+ throw new Error(`this goal is waiting on you — ${waiting}`);
2251
+ }
2252
+ }
2253
+ /**
2254
+ * ⛔ AND HAS THIS MACHINE ALREADY DECIDED THAT REPEATING IS USELESS?
2255
+ *
2256
+ * ───────────────────────────────────────────────────────────────────────────
2257
+ * THE BYPASS THIS CLOSES
2258
+ *
2259
+ * A hand-typed `looply run` never touches the autonomy controller. With no
2260
+ * `--command-id` and no `--request-attempt` the whole admission block in
2261
+ * `bin.ts` is skipped, so nothing consults a grant, a budget or a safety
2262
+ * hold — the daemon's brake is simply not on this path. Without this fence a
2263
+ * person could be told "I have stopped, nothing is working" and then re-enter
2264
+ * the identical feedback loop by typing eight characters.
2265
+ *
2266
+ * ───────────────────────────────────────────────────────────────────────────
2267
+ * WHY AN OVERRIDE NEEDS NOTHING SPECIAL HERE
2268
+ *
2269
+ * A person's explicit Start is the escape hatch, and it arrives through the
2270
+ * daemon: the Start gate spends the incident's one retry BEFORE spawning the
2271
+ * run, so by the time this reads the store the incident is no longer ACTIVE
2272
+ * and the authorised run walks through. A hand-typed run has spent nothing,
2273
+ * so it meets the hold — which is the whole distinction, and it needs no flag
2274
+ * on the command line to express. ARGV PROVES NOTHING HERE EITHER.
2275
+ *
2276
+ * ⚠️ IT IS A PLAIN ERROR, LIKE THE APPROVAL FENCE ABOVE, not a RunRefused:
2277
+ * every refusal code is terminal, and ending a person's grant because their
2278
+ * machine is stuck would answer a question nobody asked.
2279
+ *
2280
+ * ⚠️ HONEST LIMIT: this binds the BOUND path. An unbound hand-typed run has no
2281
+ * pursuit to be held for, and nothing on that path can clear or spend an
2282
+ * incident — asserted in the spec rather than assumed here.
2283
+ */
2284
+ /**
2285
+ * THE RESOURCE SURFACES THIS RUN DECLARED, TAKEN IN A TOTAL ORDER.
2286
+ *
2287
+ * 🔴 ALL OR NOTHING, BEFORE ANY WORK. Holding two of three surfaces while
2288
+ * waiting for the third is precisely the state a total acquisition order
2289
+ * exists to make impossible — which is why deadlock here is unrepresentable
2290
+ * rather than detectable, and why there is no wait-for graph to write.
2291
+ *
2292
+ * ⚠️ AND `run.surfaces` OFF MEANS NO LEASE IS EVER TAKEN. Not "taken and
2293
+ * ignored": the acquire is not called at all, so a project that never asked
2294
+ * for surfaces has no new file, no new failure mode and no new refusal.
2295
+ */
2296
+ const declaredSurfaces = (0, declared_surfaces_1.declaredSurfacesFor)(config, binding);
2297
+ let heldSurfaces = [];
2298
+ if (declaredSurfaces.length > 0) {
2299
+ const got = await holding(() => {
2300
+ faulting('acquireSurfaces');
2301
+ return (0, surface_lease_1.acquireSurfaces)(cwd, declaredSurfaces, { runId, ttlMs: SURFACE_TTL_MS });
2302
+ });
2303
+ /**
2304
+ * ⚠️ A PLAIN THROW, NOT A `RunRefused`. Every refusal code is TERMINAL — a
2305
+ * controller seeing one would stop offering this pursuit — and "somebody
2306
+ * else is holding a directory" is the most retryable condition there is.
2307
+ *
2308
+ * ⚠️ AND THE RELEASE IS ON THE LINE ABOVE THE THROW, not separated from it
2309
+ * by prose. A source pin reads a window around every `throw` in this
2310
+ * function looking for `releaseHold()`, because a throw that skips it leaks
2311
+ * the lease, the registry entry and the slot — and a comment between the
2312
+ * two is enough to make the pin miss it.
2313
+ */
2314
+ if (!got.ok) {
2315
+ leaving(`cannot start: ${got.because}`);
2316
+ await releaseHold();
2317
+ throw new Error(`cannot start: ${got.because}`);
2318
+ }
2319
+ heldSurfaces = got.held;
2320
+ // Visible to `releaseHold`, which every refusal path already calls and
2321
+ // which is defined above this line. See its surface branch.
2322
+ heldSurfacesRef.current = got.held;
2323
+ log(`surface : holding ${declaredSurfaces.join(', ')}`);
2324
+ }
2325
+ /** The hold whose one retry this run has been told to spend, once work starts. */
2326
+ let retryToSpend = null;
2327
+ if (binding.kind === 'bound' && safety !== null) {
2328
+ const incident = safety.activeIncident(binding.goal.id, binding.goal.version);
2329
+ if (incident !== null) {
2330
+ /**
2331
+ * ⚠️ THE SPEND IS ASKED FOR, NEVER ASSUMED. Without the flag this is the
2332
+ * refusal it has always been — the brake does not release itself because
2333
+ * somebody ran the command again.
2334
+ */
2335
+ if (opts.tryOnceMore !== true) {
2336
+ log(`action : not starting work — ${incident.because}`);
2337
+ leaving(`this machine has stopped working on that goal — ${incident.because}`);
2338
+ await releaseHold();
2339
+ throw new Error(`this machine has stopped working on that goal — ${incident.because}. ` +
2340
+ 'Run it again with --try-once-more to spend this incident’s one retry.');
2341
+ }
2342
+ /**
2343
+ * 🔴 THE FENCE IS HERE; THE SPEND IS NOT. IT HAPPENS JUST BEFORE THE
2344
+ * FIRST ROUND, AND THE DISTANCE BETWEEN THE TWO IS WHY.
2345
+ *
2346
+ * Spending here burnt the retry about a thousand lines before any work
2347
+ * could begin, and several ordinary things refuse a run in between — a
2348
+ * dirty working tree ("commit or stash first", thrown as the first thing
2349
+ * `runner.run()` does), an empty work set, a superseded goal. So a person
2350
+ * with three uncommitted edits — which is how people at terminals live —
2351
+ * typed the command looply told them to, watched it print "one retry,
2352
+ * spent", and then watched it die without buying a single model turn. The
2353
+ * retry was gone and the hold was permanently discharged.
2354
+ *
2355
+ * The refusal has to be early, because the point is not to start. The
2356
+ * SPEND has to be late, because the point is to buy an attempt.
2357
+ */
2358
+ retryToSpend = incident;
2359
+ log(`action : held by ${incident.kind} (${incident.id}) — its one retry will be spent when work starts`);
2360
+ }
2361
+ }
2362
+ // One agent runner for all three roles. The prompt travels on stdin, so a
2363
+ // multi-line prompt, a diff full of quotes, and a prompt past cmd.exe's 8191
2364
+ // character ceiling all stop being the runner's problem (agent-exec.ts).
2365
+ // MEDIATED. The model runs outside the container — the packaged provider
2366
+ // cannot start inside one — and gets no host tools at all. Every capability
2367
+ // it can call is a Looply MCP tool, and each of those executes contained.
2368
+ /**
2369
+ * WHICH PROVIDER BINARY IS ABOUT TO ANSWER.
2370
+ *
2371
+ * 🔴 Until now the only test applied to it was `existsSync`, and nothing
2372
+ * recorded what a run had used — so an update, a tamper, or a different Node
2373
+ * install shifting the derived path all looked identical to nothing having
2374
+ * happened. This says so once, and only when it CHANGED.
2375
+ *
2376
+ * Best-effort by construction: a diagnostic that could stop a run would be a
2377
+ * worse thing than the gap it reports.
2378
+ */
2379
+ await (async () => {
2380
+ try {
2381
+ /**
2382
+ * 🔴 `resolved.spec.command`, NOT `config.agent.command` — AND THE
2383
+ * DIFFERENCE MADE THIS RECORD LIE.
2384
+ *
2385
+ * The transport launches `resolved.spec.command`; this asked the CONFIG.
2386
+ * A preset resolves one to the other, so a project configured for a
2387
+ * different provider could be recorded as the default — `provider: codex`
2388
+ * hashing `claude.exe` — which is precisely the "never silently swap
2389
+ * model or provider" invariant, broken in the one record kept to detect
2390
+ * it.
2391
+ *
2392
+ * The rule is that identity is a fact about what WILL RUN, so it is read
2393
+ * from the thing that will run it.
2394
+ */
2395
+ const isDefault = resolved.spec.command === agent_exec_1.DEFAULT_AGENT.command;
2396
+ const pkg = isDefault ? (0, agent_contained_1.claudePackageDir)() : null;
2397
+ const note = await (0, provider_identity_store_1.noteProviderIdentity)(new provider_identity_store_1.ProviderIdentityStore(stateDir, ledger_store_1.nodeLedgerFs), isDefault
2398
+ ? { kind: 'default', ...(pkg === null ? {} : { executable: (0, node_path_1.join)(pkg, 'bin', 'claude.exe') }) }
2399
+ : { kind: 'configured', command: resolved.spec.command });
2400
+ if (note !== null)
2401
+ log(`provider: ${note}`);
2402
+ }
2403
+ catch {
2404
+ /* a diagnostic must never be the reason a run does not start */
2405
+ }
2406
+ })();
2407
+ /**
2408
+ * ═══════════════════════════════════════════════════════════════════════
2409
+ * THE CANONICAL EXECUTION ADMISSION. EVERY CHARGEABLE RUN PASSES HERE.
2410
+ * ═══════════════════════════════════════════════════════════════════════
2411
+ *
2412
+ * 🔴 IT IS HERE, AND NOWHERE ELSE, BECAUSE `runLoop` IS THE ONE THING EVERY
2413
+ * RUN IS.
2414
+ *
2415
+ * A person typing `looply run`, a Start the daemon spawned, a goal the
2416
+ * coordinator placed in a workspace, and anything Autopilot decides all
2417
+ * converge on this function. Checking a budget in the Brain, the coordinator,
2418
+ * the CLI and the provider adapter would be four policies that agree until
2419
+ * the day one of them is edited — and the one that spends the money is
2420
+ * whichever runs last. So there is one gate, at the lowest point, and every
2421
+ * caller above it is an optimisation rather than an authority.
2422
+ *
2423
+ * ⚠️ BEFORE THE AGENT EXISTS. The line below constructs the transport; the
2424
+ * first model call follows within the round loop. Refusing here means no
2425
+ * provider process is started, no turn is spent, and — crucially — no
2426
+ * implementation ATTEMPT is charged against the item, because the round loop
2427
+ * has not begun.
2428
+ *
2429
+ * ⚠️ AND IT IS NOT A STATEMENT ABOUT THE GOAL. A full budget is the most
2430
+ * retryable condition there is: it becomes false as the window rolls. So it
2431
+ * leaves by the same door a held surface does — a plain Error, never a
2432
+ * `RunRefused`, whose every code is terminal and would send a controller's
2433
+ * pursuit to `human_required` for a wallet that refills in an hour.
2434
+ *
2435
+ * ⚠️ NO ENVELOPE, NO GATE. Absent is what every existing project has, and
2436
+ * Simple Mode gains no new refusal by upgrading.
2437
+ */
2438
+ const envelope = config.machine?.envelope;
2439
+ let admission = null;
2440
+ if (envelope !== undefined) {
2441
+ const inFlight = (() => {
2442
+ try {
2443
+ /**
2444
+ * 🔴 THE CANDIDATE MUST NOT COUNT ITSELF, AND THIS COMMENT USED
2445
+ * TO ARGUE THAT IT SHOULD.
2446
+ *
2447
+ * `withinEnvelope` refuses when `runsInFlight >= maxConcurrentRuns`,
2448
+ * so what it is being asked is "may one MORE start" — the candidate
2449
+ * is the "more". This run registers before it asks, so passing the
2450
+ * registry's count included itself.
2451
+ *
2452
+ * MEASURED: `max_concurrent_runs` defaults to 1, so 1 >= 1 refused,
2453
+ * and ANY project that wrote a `machine.envelope:` block could no
2454
+ * longer run a single goal. A budget that bricks Simple Mode is not a
2455
+ * budget.
2456
+ */
2457
+ return Math.max(0, registry.active().length - 1);
2458
+ }
2459
+ catch {
2460
+ return 0;
2461
+ }
2462
+ })();
2463
+ const admitted = (0, spend_ledger_1.admitSpend)(envelope, binding.kind === 'bound' ? `run:${binding.goal.id}` : 'run', envelope.estimateUsdPerStart, inFlight, () => Date.now(), undefined,
2464
+ // So the settlement below REPLACES this run's rows instead of being
2465
+ // added to the very rows it is computed from.
2466
+ runId);
2467
+ if (!admitted.ok) {
2468
+ log(`budget : ${admitted.because}`);
2469
+ leaving(`cannot start: ${admitted.because}`);
2470
+ await releaseHold();
2471
+ throw new Error(`cannot start: ${admitted.because}`);
2472
+ }
2473
+ admission = admitted.admission;
2474
+ settleAdmission = () => {
2475
+ /**
2476
+ * 🔴 SETTLED FROM THIS RUN'S OWN ROWS, EXACTLY ONCE. The reservation held
2477
+ * an ESTIMATE; the transport wrote what each invocation actually cost,
2478
+ * tagged with this runId. Settling with the sum turns the promise into a
2479
+ * receipt, and `Admission.settle` is idempotent so a teardown that runs
2480
+ * twice charges once.
2481
+ *
2482
+ * ⚠️ AN UNMEASURED RUN SETTLES AT `undefined`, not at zero. The round
2483
+ * path reports no cost at all, so a run that did rounds has spent an
2484
+ * amount nobody can state — and the window must stay unmeasurable rather
2485
+ * than record a night of work as free.
2486
+ */
2487
+ const actual = (0, spend_ledger_1.spentByRun)(runId);
2488
+ admission?.settle(actual === null ? undefined : actual, Date.now() - startedAtMs);
2489
+ };
2490
+ log(`budget : admitted — the machine is inside its envelope` +
2491
+ (envelope.maxUsd === undefined ? '' : ` (ceiling $${envelope.maxUsd.toFixed(2)})`));
2492
+ }
2493
+ // `children` is this run's registry; the transport is the longest-lived
2494
+ // child it starts and was the one thing never adopted into it.
2495
+ //
2496
+ // ⚠️ `runId` TRAVELS WITH IT, so every invocation's cost is attributed to
2497
+ // this run and the admission above can be settled at what it really cost.
2498
+ /**
2499
+ * 🔴 THE JSON ENVELOPE, UNWRAPPED — SO THE ROUND'S COST IS RECORDED.
2500
+ *
2501
+ * The round asked for `--output-format text` and so recorded every
2502
+ * invocation with `usd` absent: honest, and useless — the most expensive
2503
+ * path in the system could never say what it spent, and a trial's report had
2504
+ * to write "no actual usage captured". The scanner already ran on the JSON
2505
+ * path; the round, the verifier and the planner now do too. The transport
2506
+ * records the provider's `total_cost_usd`, `num_turns`, `usage` and
2507
+ * `session_id` at the invocation, and `unwrapping` hands the round the same
2508
+ * final message it always read. Missing usage stays absent — never zero.
2509
+ */
2510
+ /**
2511
+ * ⚠️ THE BUILT-IN PROVIDER ONLY. A configured provider's stdout is the
2512
+ * model's own stream: there is no CLI between the model and this process to
2513
+ * write an envelope the model cannot forge, so asking it for "json" and
2514
+ * unwrapping the answer would let a model-typed `total_cost_usd` become a
2515
+ * ledger row — the closure acceptance check proves exactly that, end to
2516
+ * end. A configured provider is driven exactly as before: text, unwrapped
2517
+ * by nothing, recorded with `usd` absent.
2518
+ */
2519
+ const builtIn = (0, model_transport_1.isBuiltInProvider)(resolved.spec);
2520
+ const wireFormat = builtIn ? { format: 'json' } : {};
2521
+ /**
2522
+ * F35: and a usage-limit refusal is waited out inside this Start rather than returned as the
2523
+ * answer — measured live, it was read as "the compiler failed" and three Starts were bought in
2524
+ * ninety seconds. Inside `unwrapping`, because the refusal is read off the envelope it strips.
2525
+ * A Stop, a Pause or a signal ends the wait (see `provider-limit.ts`).
2526
+ */
2527
+ const limitWait = {
2528
+ now: () => Date.now(),
2529
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
2530
+ interrupted: async () => {
2531
+ if (signals.requested() || halt !== null)
2532
+ return true;
2533
+ try {
2534
+ const req = await control.read();
2535
+ return req !== null && (req.kind === 'stop' || req.kind === 'pause');
2536
+ }
2537
+ catch {
2538
+ // A control file that cannot be read now says nothing; the next slice reads it again.
2539
+ return false;
2540
+ }
2541
+ },
2542
+ log,
2543
+ // A detached run's output is discarded: the ledger is what `looply goal` and the phone read.
2544
+ announce: async (text) => {
2545
+ try {
2546
+ await ledgerForRun.append({
2547
+ ts: new Date().toISOString(),
2548
+ runId,
2549
+ projectId,
2550
+ lane: 'feature',
2551
+ kind: 'alert',
2552
+ actor: 'system',
2553
+ data: { code: 'provider-limit', severity: 'warn', text },
2554
+ });
2555
+ }
2556
+ catch (e) {
2557
+ log(`provider: the wait could not be recorded (${e.message})`);
2558
+ }
2559
+ },
2560
+ };
2561
+ const withFinalMessage = (invoke) => builtIn ? (0, agent_envelope_1.unwrapping)((0, provider_limit_1.waitingOutProviderLimits)(invoke, limitWait)) : invoke;
2562
+ const agent = withFinalMessage((0, model_transport_1.makeMediatedAgentRunner)(cwd, 'implementer', resolved.spec, { runId, ...wireFormat }, children));
2563
+ /**
2564
+ * 🔴 THE VERIFIER GETS THE VERIFIER'S TOOLS — AND A RUN USED TO LEND IT
2565
+ * THE IMPLEMENTER'S.
2566
+ *
2567
+ * One transport was built here and handed to three passes: the round, the
2568
+ * compile step, and the VERIFY pass. `TOOL_POLICIES` distinguishes them —
2569
+ * implementer gets `read, glob, grep, edit, write`, verifier gets
2570
+ * `read, glob, grep` — and `verify-cmd` builds a verifier-roled transport for
2571
+ * the standalone command. Inside a run, that narrowing was simply not applied.
2572
+ *
2573
+ * MEASURED at the bridge: a verifier-roled bridge answers "the verifier may
2574
+ * not call write"; an implementer-roled one accepts the call. So a model asked
2575
+ * to CHECK an item against the code could change the code first, and its
2576
+ * verdict would then be about a tree it had just written — in one of the two
2577
+ * keys the whole design rests on.
2578
+ *
2579
+ * ⚠️ `runId` TRAVELS WITH IT TOO, so verify-pass invocations are still
2580
+ * attributed to this run and settled with everything else it spent.
2581
+ */
2582
+ const verifyAgent = withFinalMessage((0, model_transport_1.makeMediatedAgentRunner)(cwd, 'verifier', resolved.spec, { runId, ...wireFormat }, children));
2583
+ /**
2584
+ * 🔴 PLANNING GETS THE PLANNER'S TOOLS. Compiling a goal and evaluating a
2585
+ * goal gap both READ the codebase and produce a plan; neither edits anything.
2586
+ * Both were lent the implementer's transport — `[read, glob, grep, edit,
2587
+ * write]` — for no reason beyond it being the one already in scope, which is
2588
+ * exactly how the verify pass came to carry write until it was narrowed.
2589
+ *
2590
+ * `brain` is `[read, glob, grep]`. A capability nothing uses is a capability
2591
+ * that only ever gets used by accident, and the narrowing costs nothing
2592
+ * because a compile that edits files is not a compile.
2593
+ *
2594
+ * `runId` travels with it, so planning invocations stay attributed to this
2595
+ * run and are settled with everything else it spent.
2596
+ */
2597
+ const planAgent = withFinalMessage((0, model_transport_1.makeMediatedAgentRunner)(cwd, 'brain', resolved.spec, { runId, ...wireFormat }, children));
2598
+ /**
2599
+ * 🔴 THE PROVER GETS THE PROVER'S TOOLS — THE SAME NARROWING AS THE TWO
2600
+ * ABOVE, AND THE ONE THAT WAS MISSED.
2601
+ *
2602
+ * `prove` was handed `agent`, the implementer's transport, so the second
2603
+ * key ran with `edit` and `write` in the window between a GREEN gate and
2604
+ * the commit, where nothing re-gates what it changes. Measured live on
2605
+ * 2026-09-24: the bridge was launched as `implementer` for the proving turn,
2606
+ * and its spend was booked as implementer spend. See `TOOL_POLICIES.prover`.
2607
+ */
2608
+ const proveAgent = withFinalMessage((0, model_transport_1.makeMediatedAgentRunner)(cwd, 'prover', resolved.spec, { runId, ...wireFormat }, children));
2609
+ /**
2610
+ * A goal with nothing to work on compiles itself some work.
2611
+ *
2612
+ * Here, and not in RoundRunner, because this is the composition root: the
2613
+ * goal is already resolved, the lease is already held, and the agent already
2614
+ * exists. The runner receives a queue and cannot tell why anything is in it,
2615
+ * which is the property the whole scoped-queue design exists to preserve.
2616
+ *
2617
+ * ONLY when the goal owns nothing. That is the whole idempotency story at
2618
+ * this level: derived ids make a materialisation retry converge, but they
2619
+ * cannot make two separate reasoning calls agree, so the second call must not
2620
+ * happen. A goal that already owns work — because a previous run compiled it,
2621
+ * or because this Start named items to adopt — is left alone. Re-planning a
2622
+ * goal whose work is under way is a later phase's decision, not a side effect
2623
+ * of starting it again.
2624
+ *
2625
+ * A failed compilation is not a failed run: the run carries on and reports
2626
+ * "nothing to do" if that is the truth, which leaves the start-run to be
2627
+ * offered again rather than terminally consumed.
2628
+ */
2629
+ // Compile when there is no plan for THIS version, and nothing eligible that
2630
+ // could still move. Those are two different conditions and both matter:
2631
+ //
2632
+ // - no current plan, because a previous version's plan must not stop this
2633
+ // version ever getting one. That count was the whole defect.
2634
+ // - nothing eligible still working, because work a person adopted by hand
2635
+ // should be got on with rather than planned around.
2636
+ //
2637
+ // A finished current plan therefore does NOT trigger a second one. Deciding
2638
+ // whether a finished plan should be replaced is recompilation, and it is a
2639
+ // separate question with its own answer.
2640
+ const owned = queue.list();
2641
+ const eligible = runQueue.list();
2642
+ const split = (0, goal_ownership_1.splitGoalWork)(owned, scope);
2643
+ // The decision reads as the question it is asking. `split` below is for
2644
+ // saying what is being held back, which is a different job.
2645
+ const verdict = (0, goal_ownership_1.decideCompile)(split, eligible, scope);
2646
+ if (scope !== null && split.olderPlans.length) {
2647
+ // Said out loud rather than left to be inferred from a queue that looks
2648
+ // shorter than it is. These are durable and still owned; they are simply
2649
+ // not this version's work.
2650
+ log(`goal : ${split.olderPlans.length} item(s) from an earlier plan for this goal, kept but not worked`);
2651
+ }
2652
+ if (scope !== null && split.unknownPlans.length) {
2653
+ // Compiler output from before provenance was recorded. Durable, inert, and
2654
+ // NOT treated as hand-adopted work: it was compiled for some particular
2655
+ // version and nobody knows which, so it cannot be version-independent.
2656
+ log(`goal : ${split.unknownPlans.length} compiled item(s) here predate version tracking — ` +
2657
+ 'kept, but not worked by any version');
2658
+ }
2659
+ if (scope !== null && split.forged.length) {
2660
+ log(`goal : ${split.forged.length} item(s) here carry a compiler version but did not come from the ` +
2661
+ 'compiler — ignoring them');
2662
+ }
2663
+ if (scope !== null && split.newerPlans.length) {
2664
+ /**
2665
+ * REFUSE, rather than work around it.
2666
+ *
2667
+ * Goal versions only ever go up, so a plan compiled for a LATER version is
2668
+ * durable evidence against the authority this run is acting under — a
2669
+ * rolled-back mirror, a Start that should have been refused, a hand-edited
2670
+ * queue. It should be unreachable: a Start behind the mirror is terminally
2671
+ * refused long before selection.
2672
+ *
2673
+ * Merely hiding it and working something else is the unsafe part. The
2674
+ * machine would be executing under an authority it has proof is stale,
2675
+ * which is exactly what the version rules exist to prevent — so this stops,
2676
+ * terminally, and touches nothing. The offending items are left exactly as
2677
+ * they are: not deleted, not un-owned, not rewritten.
2678
+ */
2679
+ if (sync)
2680
+ await sync.stop();
2681
+ signals.uninstall();
2682
+ const versions = [...new Set(split.newerPlans.map((i) => i.compiledForGoalVersion))].join(', ');
2683
+ const superseded = `this machine holds work compiled for a LATER version of this goal (${versions}, and this Start says ` +
2684
+ `${binding.kind === 'bound' ? binding.goal.version : '?'}) — refusing to run against evidence that ` +
2685
+ 'the goal has already moved on';
2686
+ leaving(superseded);
2687
+ await releaseHold();
2688
+ throw new RunRefused(superseded, 'goal_superseded');
2689
+ }
2690
+ /**
2691
+ * Whether this Start has already paid for a compilation.
2692
+ *
2693
+ * The adaptive decision below is only for a Start that arrived with an
2694
+ * EXISTING exhausted plan. A Start that compiled its first plan has already
2695
+ * spent ten minutes of compiler and up to thirty of verify against a
2696
+ * sixty-minute window, and adding an evaluation and a second compilation to
2697
+ * that would run past it — the command would be marked expired while the run
2698
+ * carried on unacked, which is the failure Phase 4.1 exists to prevent.
2699
+ */
2700
+ let compiledThisRun = false;
2701
+ /**
2702
+ * Verification is budgeted for the WHOLE process, not per pass.
2703
+ *
2704
+ * There are now two passes on the adaptive path — the one below, and one over
2705
+ * the plan the adaptive decision may produce. `VERIFY_MAX_PER_RUN` was
2706
+ * written when there could only be one, so two passes each taking six drafts
2707
+ * would put sixty minutes of verifier inside a sixty-minute window. They draw
2708
+ * from one budget instead.
2709
+ */
2710
+ let verifyBudget = verify_cmd_2.VERIFY_MAX_PER_RUN;
2711
+ /**
2712
+ * EVERYTHING A START DOES BEFORE THE RUN, under one guard.
2713
+ *
2714
+ * The compile, the verify passes and the adaptive decision all happen above
2715
+ * the try/finally that protects the run itself, so anything thrown by any of
2716
+ * them — a rejected agent invocation being the likely one — would leave this
2717
+ * process holding the lease and skip the teardown entirely. Recoverable,
2718
+ * because the next process finds the pid dead, but it makes an ordinary agent
2719
+ * failure look like a crash and wedges the tree until something notices.
2720
+ *
2721
+ * The guard was first put around the adaptive seam alone, which left its two
2722
+ * older neighbours exposed for exactly the same reason.
2723
+ *
2724
+ * Nothing here throws. What it would have thrown is carried to the teardown,
2725
+ * which releases the lease and reports it as retryable.
2726
+ */
2727
+ /** Hoisted: both are read by the teardown, which sits outside the guard. */
2728
+ /**
2729
+ * M3-2 — THE COMPILER'S ANSWER IS CHECKED, AND THE COMPILE IS A RECORD.
2730
+ *
2731
+ * The prompt defines a step's evidence as a claim about code that already
2732
+ * exists, so every entry is re-read at the committed tree before the step
2733
+ * becomes an item: a refused step is logged with the claim beside the fact
2734
+ * and never queued. Admitted or refused, the compile is appended to
2735
+ * `compiles.jsonl` — who was asked (the provider's identity, read from the
2736
+ * thing that runs, as the identity note above does), with what (a digest,
2737
+ * never the prompt or the transcript), at which tree, what came back and
2738
+ * what was refused — BEFORE any item is minted. A record that cannot be
2739
+ * written is a log line, never a refused run.
2740
+ */
2741
+ const compileReaders = (0, citation_readers_1.nodeCitationReaders)(cwd, stateDir);
2742
+ const compileProvider = (0, provider_identity_1.providerRecordFor)(resolved.spec, resolved.spec.command === agent_exec_1.DEFAULT_AGENT.command ? (0, agent_contained_1.claudePackageDir)() : null);
2743
+ const compiles = new compile_store_1.CompileStore(stateDir, ledger_store_1.nodeLedgerFs);
2744
+ const recordCompile = async (forGoalId, forGoalVersion, treeSha, outcome, checked) => {
2745
+ try {
2746
+ if (!compiles.isOpen())
2747
+ await compiles.open();
2748
+ await compiles.append((0, core_2.compileRecordFrom)({
2749
+ goalId: forGoalId,
2750
+ goalVersion: forGoalVersion,
2751
+ treeSha,
2752
+ requestDigest: outcome.requestDigest,
2753
+ provider: compileProvider,
2754
+ askedAt: new Date().toISOString(),
2755
+ answer: outcome.ok ? 'plan' : outcome.failure === 'unreadable' ? 'malformed' : 'no-answer',
2756
+ ...(outcome.ok ? {} : { because: outcome.reason }),
2757
+ admitted: checked?.admitted.length ?? 0,
2758
+ refused: (checked?.refused ?? []).map((r) => ({ title: r.step.title, because: r.verdict.ok ? 'admitted' : r.verdict.because })),
2759
+ malformed: outcome.ok ? outcome.malformed : 0,
2760
+ dropped: outcome.ok ? outcome.dropped : 0,
2761
+ }));
2762
+ }
2763
+ catch (e) {
2764
+ log(`compile : the record could not be written — ${e.message}`);
2765
+ }
2766
+ };
2767
+ let pickable = 0;
2768
+ let adaptive = null;
2769
+ /**
2770
+ * D3's answer, computed ONCE when a `satisfied` decision lands.
2771
+ *
2772
+ * Held rather than recomputed because one of the two endings that render it
2773
+ * (`nothingPickableBecause`) is synchronous, and because asking the server
2774
+ * twice could give two answers to a question whose whole purpose is to be
2775
+ * decided once. Undefined until a `satisfied` decision exists, and undefined
2776
+ * for ever if this run never reaches one.
2777
+ */
2778
+ let coverage;
2779
+ /**
2780
+ * What KEY 2 actually observed, kept from the call that established it.
2781
+ *
2782
+ * Null until KEY 2 turns, and null for ever if it never does — in which case
2783
+ * the fourth key is never asked either, so there is nothing to be missing.
2784
+ */
2785
+ let key2Saw = null;
2786
+ /** The run whose round KEY 2 confirmed — an EARLIER Start's, not this one's (see `deliveringBranch`). */
2787
+ let deliveringRunId = null;
2788
+ // L4b: the commit key 2 confirmed HEAD on, beside the run that delivered it.
2789
+ let deliveringSha = null;
2790
+ /** F27: the tree this Start's evaluator judged, when a programme moved HEAD to the tree the lane holds. */
2791
+ let judged = { kind: 'as-is', because: 'no goal-met check ran' };
2792
+ /** The first compile's authority answer, kept so its reason is the acted-on one. */
2793
+ let stillOurs = { ok: true };
2794
+ let seamFailure = null;
2795
+ try {
2796
+ if (verdict.compile && scope !== null && binding.kind === 'bound') {
2797
+ compiledThisRun = true;
2798
+ log(`compile : ${binding.goal.text}`);
2799
+ const compiled = await (0, goal_compile_1.compileGoal)({
2800
+ goalId: binding.goal.id,
2801
+ goalVersion: binding.goal.version,
2802
+ goalText: binding.goal.text,
2803
+ projectId,
2804
+ projectName: config.project.name,
2805
+ /**
2806
+ * 🔴 THE POINT OF THE WHOLE PHASE, AT THE ONE LINE THAT MAKES IT REAL.
2807
+ * Before this, the compiler received five fields and none of them was
2808
+ * history, so a plan was recompiled from the goal text every time and
2809
+ * could propose an approach that had already failed three times.
2810
+ *
2811
+ * Read at the COMMITTED tree, so a content fact observed at a tree
2812
+ * that has since moved is not offered as current — the reader decides
2813
+ * currency, and this is where it is given what it needs to.
2814
+ */
2815
+ memory: await memoryFor(memoryStore, git, { goalId: binding.goal.id }, await currentGraph()),
2816
+ // The task packet (M2-3), after memory: the requirements a person linked
2817
+ // this goal to, quoted as data with their standing, one server read per
2818
+ // linked source; an unanswerable source is a `missing` line, not a refusal.
2819
+ ...(goalServer === null
2820
+ ? {}
2821
+ : {
2822
+ packet: (await (0, packet_for_goal_1.packetForGoal)(binding.goal, (id) => goalServer.sources.sourceDetail(projectId, id),
2823
+ // F19: in a programme, what the OTHER goals own is named so this plan does none of it.
2824
+ (0, programme_mode_1.programmeIsOn)(config) ? goals.all() : [])).packet,
2825
+ // The checks a claim may cite: the gate's step names ARE the
2826
+ // acceptance map `produceClaims` holds `provenBy` against.
2827
+ checks: config.gates.map((g) => g.name),
2828
+ }),
2829
+ }, { invoke: planAgent });
2830
+ // M3-2: checked at the committed tree before any step becomes an item; the compile recorded either way.
2831
+ const compiledAt = await git.headTreeSha().catch(() => null);
2832
+ const checked = compiled.ok ? await (0, compile_check_1.checkCompiledPlan)(compiled.steps, compileReaders, compiledAt) : null;
2833
+ for (const r of checked?.refused ?? [])
2834
+ log(`compile : ${(0, compile_check_1.describeRefusal)(r)}`);
2835
+ await recordCompile(binding.goal.id, binding.goal.version, compiledAt, compiled, checked);
2836
+ if (!compiled.ok) {
2837
+ // Retryable, and nothing durable: a compiler that failed did not produce
2838
+ // a plan, so this version still has none and the next Start tries again.
2839
+ log(`compile : ${compiled.reason} — this run has nothing to work on.`);
2840
+ compileNote = `compile: ${compiled.reason} — this run has nothing to work on`;
2841
+ }
2842
+ else if (!compiled.steps.length && compiled.malformed > 0) {
2843
+ /**
2844
+ * NOT an empty plan: an UNREADABLE one.
2845
+ *
2846
+ * `extractPlan` drops steps it cannot read and carries on, so a
2847
+ * transcript of malformed objects arrives here as `ok: true` with no
2848
+ * steps and a non-zero `dropped`. That is indistinguishable from a
2849
+ * legitimate empty plan by the step count alone — and the difference
2850
+ * matters, because a receipt would durably spend revision 1 on an answer
2851
+ * nobody could read.
2852
+ *
2853
+ * Nothing is written, so the next Start compiles the same revision again,
2854
+ * which is what a retryable failure should do.
2855
+ */
2856
+ log(`compile : the plan could not be read — ${compiled.malformed} step(s) were malformed. ` +
2857
+ 'Nothing queued, and this goal still has no plan.');
2858
+ compileNote = `compile: the plan could not be read — ${compiled.malformed} step(s) were malformed`;
2859
+ }
2860
+ else if (checked !== null && !checked.admitted.length && checked.refused.length > 0) {
2861
+ // Every step made a claim about the repository that does not hold. Like a
2862
+ // plan that could not be read: nothing queued, no plan for this version,
2863
+ // and the next Start tries again.
2864
+ log(`compile : every step was refused — ${checked.refused.length} step(s) cited evidence that does not hold at the tree. ` +
2865
+ 'Nothing queued, and this goal still has no plan.');
2866
+ compileNote = `compile: every step was refused — ${checked.refused.length} step(s) cited evidence that does not hold at the tree`;
2867
+ }
2868
+ else if (!(stillOurs = await freshAuthority(binding.goal.id, binding.goal.version, binding.goal.text)).ok) {
2869
+ /**
2870
+ * The goal moved while its FIRST plan was being compiled.
2871
+ *
2872
+ * The same ten-minute window the adaptive path guards, on the path that
2873
+ * had no guard at all. `binding` is resolved once, before the lease is
2874
+ * even taken, and the daemon rewrites goals.json on its own cadence
2875
+ * holding no lease — so a person who amends or abandons the goal three
2876
+ * minutes into a compile is invisible to a run that only remembers.
2877
+ *
2878
+ * Nothing is written. The version stamped on those steps would be one
2879
+ * nobody is pursuing, and a run that materialised them would then work
2880
+ * them for hours: `scope` still says the old version, so they classify
2881
+ * as current-plan, the verify pass promotes them, and the release
2882
+ * command for an abandoned goal cannot even land — it needs the lease
2883
+ * this run is holding.
2884
+ *
2885
+ * Asked ONCE and its answer kept: two reads could disagree, and the
2886
+ * reason a person is given must be the reason the run acted on.
2887
+ */
2888
+ log(`compile : this goal moved while its plan was being made — ${stillOurs.reason}`);
2889
+ compileNote = `compile: this goal moved while its plan was being made — ${stillOurs.reason}`;
2890
+ }
2891
+ else {
2892
+ const now = Date.now();
2893
+ /**
2894
+ * The receipt goes in whatever the plan contained, INCLUDING nothing.
2895
+ *
2896
+ * An empty plan is a legitimate answer — the compiler read the goal and
2897
+ * had no steps to propose — and it used to leave no trace at all. So the
2898
+ * next Start found a version that owned nothing, `decideCompile` said
2899
+ * "compile", and it asked the same question again. For ever, once per
2900
+ * explicit Start, at ten minutes a time.
2901
+ *
2902
+ * The receipt is what makes "revision 1 happened" true independently of
2903
+ * what revision 1 contained. It applies to the FIRST plan for the same
2904
+ * reason it applies to the adaptive ones; leaving R1 out was the only
2905
+ * thing that made an empty first plan different from an empty second one.
2906
+ */
2907
+ const firstSteps = checked?.admitted ?? [];
2908
+ /**
2909
+ * HOISTED so the claims can be attributed to the items these steps
2910
+ * became. `planToItems` maps one step to one item IN ORDER, and
2911
+ * `produceClaims` relies on that alignment; computing it inline would
2912
+ * have left nothing to align against.
2913
+ */
2914
+ const firstItems = (0, goal_compile_1.planToItems)(firstSteps, {
2915
+ goalId: scope.goalId,
2916
+ goalVersion: binding.goal.version,
2917
+ projectId,
2918
+ orderKeyAt: (i) => (0, core_1.newId)('item', now + i + 1),
2919
+ });
2920
+ const written = await (0, goal_ownership_1.materialiseGoalPlan)(queue, scope.goalId, [
2921
+ (0, goal_compile_1.planMarker)({
2922
+ goalId: scope.goalId,
2923
+ goalVersion: binding.goal.version,
2924
+ revision: core_1.FIRST_PLAN_REVISION,
2925
+ projectId,
2926
+ orderKey: (0, core_1.newId)('item', now),
2927
+ }),
2928
+ // Time-ordered keys, unlike the ids: the loop works top-down, so order
2929
+ // is priority, and a derived id sorts arbitrarily.
2930
+ //
2931
+ // NOTE the ids here are unchanged. `planToItems` at revision 1 writes
2932
+ // no `compiledForPlanRevision` and derives through `compiledItemId`, so
2933
+ // a plan compiled before revisions existed and one compiled now are
2934
+ // byte-identical. Only the marker is new.
2935
+ ...firstItems,
2936
+ ]);
2937
+ /**
2938
+ * 🔴 NAME THE BRANCH'S GOAL WHERE THE LANE CAN READ IT.
2939
+ *
2940
+ * Written at materialisation, before any work, and written even when
2941
+ * this plan claims nothing: `coordinator-state.ts:341-347` needs a
2942
+ * `goalId` on the published attempt, and until this record existed the
2943
+ * only writer was a person typing `looply integrate --goal`. See
2944
+ * `goalOfBranch` for the deadlock that caused.
2945
+ */
2946
+ (0, lane_claims_1.recordLaneClaim)(cwd, {
2947
+ schema: lane_claims_1.LANE_CLAIMS_SCHEMA,
2948
+ kind: 'claims',
2949
+ at: new Date(now).toISOString(),
2950
+ branch,
2951
+ runId,
2952
+ goalId: scope.goalId,
2953
+ goalVersion: binding.goal.version,
2954
+ planRevision: core_1.FIRST_PLAN_REVISION,
2955
+ servings: await claimsFor(firstSteps, firstItems, binding.goal.requirements ?? []),
2956
+ // F28: the plan lives here, so the goal's later Starts are placed here.
2957
+ workspace: cwd,
2958
+ });
2959
+ if (!written.ok) {
2960
+ log(`compile : ${written.problem}`);
2961
+ compileNote = `compile: ${written.problem}`;
2962
+ }
2963
+ else if (!compiled.steps.length) {
2964
+ log('compile : the plan came back empty — nothing to do for this goal.');
2965
+ compileNote = 'compile: the plan came back empty';
2966
+ }
2967
+ else {
2968
+ log(
2969
+ // Less one for the receipt, which is not a step and must not be
2970
+ // counted to a person as though it were work queued for them.
2971
+ `compile : ${Math.max(0, written.created - 1)} step(s) queued` +
2972
+ (written.alreadyThere ? `, ${written.alreadyThere} already here` : '') +
2973
+ (compiled.dropped ? `, ${compiled.dropped} trimmed` : '') +
2974
+ (checked?.refused.length ? `, ${checked.refused.length} refused` : ''));
2975
+ }
2976
+ }
2977
+ }
2978
+ // Drafts are checked here rather than refused. A scan writes items in
2979
+ // `draft` because a finding is a claim; a run picks only `ready`. Nothing
2980
+ // joined the two, so a freshly scanned project could not be run at all — the
2981
+ // refusal pointed at the phone, which is no help unpaired and wrong paired,
2982
+ // since marking an item ready by hand skips the very check that makes it
2983
+ // pickable. Verification is the cheapest agent work looply does, so a run
2984
+ // does it rather than sending the user away.
2985
+ // Read fresh rather than from the snapshot taken before recovery: an item
2986
+ // this run just put back is pickable, and counting the stale figure made a
2987
+ // recovered run report "nothing to do" about a queue it had just repaired.
2988
+ // The SCOPED queue, not the raw one. A goal-bound run counting every ready
2989
+ // item in the project would decide it had work when none of it was its own —
2990
+ // skipping the verify pass over its own drafts, and then handing the runner a
2991
+ // scoped queue with nothing pickable in it.
2992
+ pickable = runQueue.list().filter((i) => i.state === 'ready').length;
2993
+ if (!pickable) {
2994
+ // Read fresh: `all` was taken before recovery and before compilation, so it
2995
+ // does not contain the plan this run may have just written.
2996
+ const drafts = runQueue.list().filter((i) => i.state === 'draft');
2997
+ if (drafts.length) {
2998
+ log(`verify : ${drafts.length} unchecked item(s) — confirming each against the code before picking`);
2999
+ const pass = await (0, verify_pass_1.verifyDrafts)({
3000
+ queue: runQueue,
3001
+ ledger: ledgerForRun,
3002
+ verify: (0, verify_cmd_1.makeItemVerifier)(verifyAgent, queue),
3003
+ cachePut: (id, head, ev) => evidence.put(id, head, ev),
3004
+ headSha: () => git.headSha(),
3005
+ projectId,
3006
+ now: () => Date.now(),
3007
+ onVerdict: (item, outcome) => {
3008
+ const mark = outcome.verdict === 'confirmed' || outcome.verdict === 'amended' ? 'ready' : outcome.verdict;
3009
+ log(`verify : ${mark.padEnd(9)} ${item.title.slice(0, 68)}`);
3010
+ },
3011
+ },
3012
+ // Bounded, because the whole pass happens BEFORE `run.started` — which
3013
+ // is the only thing that acks the start-run that asked for this run,
3014
+ // and that command expires an hour after it was issued. Sequential at
3015
+ // up to five minutes a draft, an uncapped pass over enough of them
3016
+ // spends the entire window and the command is marked expired while the
3017
+ // run carries on unacked. Drafts past the cap are not lost; the next
3018
+ // run checks them.
3019
+ { max: verifyBudget });
3020
+ verifyBudget -= pass.checked;
3021
+ log(`verify : ${pass.ready} of ${pass.checked} are ready to work on`);
3022
+ pickable = pass.ready;
3023
+ }
3024
+ }
3025
+ /**
3026
+ * THE ADAPTIVE DECISION, and the only place it can live.
3027
+ *
3028
+ * Below this, the block that reports "nothing to do" has already stopped the
3029
+ * sync, uninstalled the signal handlers and RELEASED THE LEASE — it is
3030
+ * teardown, not a decision point. Anything that evaluates a goal or writes a
3031
+ * plan has to happen while the tree is still held, which is here: after the
3032
+ * verify pass has had its say about the drafts, and before any of that is
3033
+ * given back.
3034
+ *
3035
+ * `pickable` is re-tested rather than assumed. Line above closes the verify
3036
+ * block, and a pass that promoted a draft leaves work to do — this must not
3037
+ * fire on a run that has something to get on with.
3038
+ *
3039
+ * ONE START, AT MOST ONE RUN: this may produce one more plan for THIS run to
3040
+ * work, and it is never reached again afterwards. `runner.run()` returns to a
3041
+ * `finally` that does nothing but clean up.
3042
+ */
3043
+ if (!pickable && scope !== null && binding.kind === 'bound' && !compiledThisRun) {
3044
+ const bound = binding.goal;
3045
+ // Captured, because `scope` moves when a new plan is materialised below and
3046
+ // the callbacks here must go on describing the goal they were built for.
3047
+ const goalId = scope.goalId;
3048
+ const owned = queue.list();
3049
+ const revision = (0, goal_ownership_1.currentPlanRevision)(owned, scope.goalId, bound.version);
3050
+ // Only for a version that already HAS a plan. With none, `decideCompile`
3051
+ // would have compiled one above, and there is nothing to have been
3052
+ // insufficient.
3053
+ if (revision > 0) {
3054
+ log(`goal : plan ${revision} has nothing left to work on — checking whether the goal is met`);
3055
+ /**
3056
+ * The scope KEY 2 is asked about, built HERE and used below.
3057
+ *
3058
+ * `scope` at this point still has no `planRevision` — it is only assigned
3059
+ * when a new plan is materialised, sixty lines down. Passing it to the
3060
+ * completion check made that check revision-BLIND: `classifyForScope`
3061
+ * treats a scope without a revision as "every plan for this version is
3062
+ * current", so revision 1's delivery could answer for revision 2. The
3063
+ * unit tests could not see it, because they supplied the revision the
3064
+ * composition root did not.
3065
+ */
3066
+ const askedScope = { ...scope, planRevision: revision };
3067
+ /**
3068
+ * F27: IN A PROGRAMME WHOSE LANE INTEGRATES, JUDGE THE TREE THE LANE HOLDS for this
3069
+ * goal's delivered work — the delivered round, the lane's replay of it, or the moved
3070
+ * base it could not be carried onto (`goal-met-target.ts`). Only in a checkout looply
3071
+ * made: moving HEAD in a person's own checkout is not this run's to do.
3072
+ */
3073
+ judged = { kind: 'as-is', because: 'not a programme workspace' };
3074
+ // F33: a checkout LOOPLY MADE (its provisioning marker), not merely any linked worktree — a person's
3075
+ // own `git worktree add` checkout inherits the tracked config and must never have its HEAD moved.
3076
+ if ((0, programme_mode_1.programmeIsOn)(config) && config.run.integration === true && (0, reaper_1.hasProvisionedMarker)(cwd)) {
3077
+ judged = await (0, goal_met_target_1.goalMetTarget)({ cwd, git, stateDir, scope: askedScope, items: owned });
3078
+ if (judged.kind !== 'as-is' && judged.sha !== (await git.headSha())) {
3079
+ if ((await git.changedPaths()).length > 0) {
3080
+ judged = { kind: 'as-is', because: 'the working tree has uncommitted changes' };
3081
+ }
3082
+ else {
3083
+ (0, goal_met_target_1.writeJudgingMarker)(stateDir, { runId, sha: judged.sha });
3084
+ await git.switchDetached(judged.sha);
3085
+ }
3086
+ }
3087
+ log(`goal : judging ${(0, goal_met_target_1.describeTarget)(judged)}`);
3088
+ }
3089
+ adaptive = await (0, goal_adaptive_1.decideAdaptive)({
3090
+ goalId: scope.goalId,
3091
+ goalVersion: bound.version,
3092
+ goalText: bound.text,
3093
+ projectId,
3094
+ projectName: config.project.name,
3095
+ currentRevision: revision,
3096
+ // The same packet and checks the first compile was given, so a
3097
+ // revision can claim what it delivers (see `AdaptiveInput.packet`).
3098
+ ...(goalServer === null
3099
+ ? {}
3100
+ : {
3101
+ packet: (await (0, packet_for_goal_1.packetForGoal)(bound, (id) => goalServer.sources.sourceDetail(projectId, id), (0, programme_mode_1.programmeIsOn)(config) ? goals.all() : [])).packet,
3102
+ checks: config.gates.map((g) => g.name),
3103
+ }),
3104
+ /**
3105
+ * Receipts filtered out — by DERIVED ID, not by tag.
3106
+ *
3107
+ * They are metadata about a plan having happened, not steps anybody
3108
+ * attempted, and listing "[closed] plan 2 for this goal" among the
3109
+ * work invites the model to reason about a row that means nothing.
3110
+ *
3111
+ * This was written as `tag !== 'goal' || state !== 'closed'` and
3112
+ * that was a real defect, the second of its kind: `'goal'` is the
3113
+ * compiler's default tag for an untagged step, and a step that
3114
+ * spends its last attempt becomes `closed`. So an untagged step
3115
+ * that FAILED three times was hidden from the evaluator, which then
3116
+ * had no reason not to say the goal was met — and seeing what the
3117
+ * plan actually did is the evaluator's whole job.
3118
+ */
3119
+ planItems: (0, goal_ownership_1.scopedQueue)(queue, askedScope)
3120
+ .list()
3121
+ .filter((i) => !(0, goal_compile_1.isPlanMarker)(i)),
3122
+ }, {
3123
+ evaluate: (i) => (0, goal_gap_1.evaluateGoalGap)(i, { invoke: planAgent }),
3124
+ compile: (i, maxSteps) => (0, goal_compile_1.compileGoal)(i, { invoke: planAgent, maxSteps }),
3125
+ changedPaths: () => git.changedPaths(),
3126
+ headSha: () => git.headSha(),
3127
+ /**
3128
+ * KEY 2, against the CURRENT revision and the FINAL commit.
3129
+ *
3130
+ * The rounds are read from the ledger at the moment the question is
3131
+ * asked, not carried from earlier in the run: an answer about
3132
+ * "was this delivered" must be about what is durably written down.
3133
+ */
3134
+ confirmSatisfied: async (head) => {
3135
+ const read = await (0, round_records_1.readRoundRecords)(stateDir, ledger_store_1.nodeLedgerFs);
3136
+ // A ledger that could not be read proves nothing EITHER WAY, and
3137
+ // since a KEY 2 refusal is terminal it must not be reported as
3138
+ // "no round delivered any of this plan". `unknown` keeps it
3139
+ // retryable and says what actually happened.
3140
+ if (!read.ok)
3141
+ return { ok: false, reason: read.because, unknown: true };
3142
+ const confirmed = (0, goal_execution_evidence_1.confirmGoalExecutionEvidence)({
3143
+ scope: askedScope,
3144
+ items: owned,
3145
+ rounds: read.rounds,
3146
+ // F27: a carried commit is confirmed as the round the lane carried.
3147
+ currentHead: judged.kind === 'carried' ? judged.of : head,
3148
+ requiredSteps: (0, gate_1.requiredSteps)(config.gates),
3149
+ });
3150
+ /**
3151
+ * 🔴 WHAT KEY 2 SAW, KEPT FOR THE FOURTH KEY.
3152
+ *
3153
+ * Per-requirement coverage asks two further questions — was the
3154
+ * item claiming a requirement in this delivery, and did the gate
3155
+ * step it names pass at this tree. Both are things this call has
3156
+ * already established, and re-deriving them below would be a
3157
+ * second answer that can disagree with the one the completion
3158
+ * claim rests on.
3159
+ */
3160
+ if (confirmed.ok) {
3161
+ deliveringRunId = confirmed.round.runId;
3162
+ deliveringSha = confirmed.round.sha ?? null;
3163
+ key2Saw = {
3164
+ delivered: new Set(confirmed.delivered),
3165
+ checks: new Map((confirmed.round.gate?.steps ?? []).map((st) => [
3166
+ st.step,
3167
+ // A `when:` filter that matched nothing is SKIPPED, which
3168
+ // is a different answer from passing — and the whole
3169
+ // reason exit 0 cannot be taken for coverage.
3170
+ st.skipped === true ? 'skipped' : st.ok ? 'passed' : 'failed',
3171
+ ])),
3172
+ };
3173
+ }
3174
+ return confirmed.ok ? { ok: true } : { ok: false, reason: confirmed.because };
3175
+ },
3176
+ stillAuthoritative: () => freshAuthority(goalId, bound.version, bound.text),
3177
+ });
3178
+ // M3-2: the adaptive plan's answer is checked and recorded the same way; a plan
3179
+ // with every step refused is no decision, not an empty revision.
3180
+ let adaptiveChecked = null;
3181
+ if (adaptive.kind === 'planned') {
3182
+ const adaptiveAt = await git.headTreeSha().catch(() => null);
3183
+ adaptiveChecked = adaptive.steps.ok ? await (0, compile_check_1.checkCompiledPlan)(adaptive.steps.steps, compileReaders, adaptiveAt) : null;
3184
+ for (const r of adaptiveChecked?.refused ?? [])
3185
+ log(`goal : ${(0, compile_check_1.describeRefusal)(r)}`);
3186
+ await recordCompile(scope.goalId, bound.version, adaptiveAt, adaptive.steps, adaptiveChecked);
3187
+ if (adaptiveChecked !== null && !adaptiveChecked.admitted.length && adaptiveChecked.refused.length > 0) {
3188
+ log(`goal : every step of plan ${adaptive.revision} was refused — ${adaptiveChecked.refused.length} step(s) cited evidence ` +
3189
+ 'that does not hold at the tree; nothing queued');
3190
+ adaptive = { kind: 'no-decision', reason: 'every step of the adaptive plan was refused' };
3191
+ }
3192
+ }
3193
+ /**
3194
+ * D3 — asked HERE, where the decision is, and only for `satisfied`.
3195
+ *
3196
+ * Every other kind already refuses for its own reason, and asking the
3197
+ * server about coverage for a goal that is not claiming to be done
3198
+ * would be a round-trip nobody reads.
3199
+ */
3200
+ if (adaptive.kind === 'satisfied') {
3201
+ /**
3202
+ * ⚠️ DEFERRED TO THE LANE WHEN THE LANE WILL JUDGE (F25). In programme mode with
3203
+ * integration, the daemon's third key judges strict coverage on the integrated tree,
3204
+ * from the lane's receipts — the acceptance authority. Judging it here, on this
3205
+ * checkout, read a receipt table nothing writes and blocked every goal. Anywhere
3206
+ * the lane does not judge, the worker still does.
3207
+ */
3208
+ coverage =
3209
+ (0, programme_mode_1.programmeIsOn)(config) && config.run.integration === true && config.machine?.integrate === true
3210
+ ? undefined
3211
+ : await coverageNow();
3212
+ /**
3213
+ * 🔴 WHAT THIS DELIVERY FINISHED, FOR THE LANE TO JUDGE LATER.
3214
+ *
3215
+ * The claims say what each item was FOR; this says which of them the
3216
+ * delivery actually finished, and at which tip. The lane cannot
3217
+ * re-derive either: no commit trailer records an item id, and the run
3218
+ * registry is released on a clean exit. It is written here because
3219
+ * this is where KEY 2 has already answered both, and a second
3220
+ * derivation could disagree with the one the completion claim rests on.
3221
+ *
3222
+ * ⚠️ IT IS PROVENANCE, NOT ACCEPTANCE. `coverage` above is this
3223
+ * worker's own verdict about its own checkout; the lane throws that
3224
+ * away and recomputes from its own gate. What travels is the claim.
3225
+ */
3226
+ await recordDelivery(scope, bound.version);
3227
+ }
3228
+ if (adaptive.kind === 'planned') {
3229
+ const at = Date.now();
3230
+ const next = adaptive.revision;
3231
+ const nextSteps = adaptiveChecked?.admitted ?? [];
3232
+ // Hoisted for the same reason as the first plan's: the claims are
3233
+ // attributed by index to the items these steps became.
3234
+ const nextItems = (0, goal_compile_1.planToItems)(nextSteps, {
3235
+ goalId: scope.goalId,
3236
+ goalVersion: bound.version,
3237
+ projectId,
3238
+ orderKeyAt: (i) => (0, core_1.newId)('item', at + i + 1),
3239
+ planRevision: next,
3240
+ });
3241
+ const written = await (0, goal_ownership_1.materialiseGoalPlan)(queue, scope.goalId, [
3242
+ // The receipt first, so a revision that produced no steps is still
3243
+ // durable and still spends its place in the budget.
3244
+ (0, goal_compile_1.planMarker)({
3245
+ goalId: scope.goalId,
3246
+ goalVersion: bound.version,
3247
+ revision: next,
3248
+ projectId,
3249
+ orderKey: (0, core_1.newId)('item', at),
3250
+ }),
3251
+ ...nextItems,
3252
+ ]);
3253
+ // One record per plan revision, for the reason the first one is
3254
+ // written: the branch's goal must be nameable from the ledger
3255
+ // whatever this revision turns out to contain.
3256
+ (0, lane_claims_1.recordLaneClaim)(cwd, {
3257
+ schema: lane_claims_1.LANE_CLAIMS_SCHEMA,
3258
+ kind: 'claims',
3259
+ at: new Date(at).toISOString(),
3260
+ branch,
3261
+ runId,
3262
+ goalId: scope.goalId,
3263
+ goalVersion: bound.version,
3264
+ planRevision: next,
3265
+ servings: await claimsFor(nextSteps, nextItems, bound.requirements ?? []),
3266
+ workspace: cwd,
3267
+ });
3268
+ if (!written.ok) {
3269
+ log(`goal : ${written.problem}`);
3270
+ }
3271
+ else {
3272
+ log(`goal : plan ${next} — ${written.created - 1} step(s) queued`);
3273
+ // The run now works the NEW plan, so its scope moves with it;
3274
+ // otherwise its own fresh rows would read as a revision it has never
3275
+ // heard of, which is a contradiction it would refuse on.
3276
+ scope = { ...scope, planRevision: next };
3277
+ runQueue = (0, goal_ownership_1.scopedQueue)(queue, scope);
3278
+ const drafts = runQueue.list().filter((i) => i.state === 'draft');
3279
+ if (drafts.length && verifyBudget > 0) {
3280
+ const pass = await (0, verify_pass_1.verifyDrafts)({
3281
+ queue: runQueue,
3282
+ ledger: ledgerForRun,
3283
+ verify: (0, verify_cmd_1.makeItemVerifier)(verifyAgent, queue),
3284
+ cachePut: (id, head, ev) => evidence.put(id, head, ev),
3285
+ headSha: () => git.headSha(),
3286
+ projectId,
3287
+ now: () => Date.now(),
3288
+ onVerdict: (item, outcome) => {
3289
+ const mark = outcome.verdict === 'confirmed' || outcome.verdict === 'amended' ? 'ready' : outcome.verdict;
3290
+ log(`verify : ${mark.padEnd(9)} ${item.title.slice(0, 68)}`);
3291
+ },
3292
+ }, { max: verifyBudget });
3293
+ verifyBudget -= pass.checked;
3294
+ log(`verify : ${pass.ready} of ${pass.checked} are ready to work on`);
3295
+ pickable = pass.ready;
3296
+ }
3297
+ }
3298
+ }
3299
+ }
3300
+ }
3301
+ }
3302
+ catch (err) {
3303
+ seamFailure = err instanceof Error ? err.message : String(err);
3304
+ log(`goal : the goal check could not be completed — ${seamFailure}`);
3305
+ }
3306
+ /**
3307
+ * M6-16: WHY A RUN WITH NOTHING PICKABLE IS LEAVING — composed from what is
3308
+ * already decided (the seam, the adaptive verdict, the work set) in the same
3309
+ * order the exits below render it, because the hold is released at the top
3310
+ * of that block and the ending is written there, before any of them.
3311
+ */
3312
+ const nothingPickableBecause = () => {
3313
+ if (seamFailure !== null)
3314
+ return `could not check this goal — ${seamFailure}`;
3315
+ const said = adaptive === null ? null : (0, goal_adaptive_1.describeAdaptive)(adaptive, attestedNow(), coverage);
3316
+ if (said !== null)
3317
+ return said.message;
3318
+ const left = scope === null ? null : (0, core_1.workLeft)(runQueue.list());
3319
+ if (left === 'finished')
3320
+ return `this goal's work is done — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`;
3321
+ if (left === 'stalled')
3322
+ return `nothing here can move without you — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`;
3323
+ return `nothing to do — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`;
3324
+ };
3325
+ if (!pickable) {
3326
+ if (sync)
3327
+ await sync.stop();
3328
+ signals.uninstall();
3329
+ // This path returns before the try/finally below, so the lease has to be
3330
+ // given back here too. Forgetting it would wedge the tree until the next
3331
+ // process noticed the pid was dead — recoverable, but for no reason.
3332
+ leaving(nothingPickableBecause());
3333
+ // F31 (corr-12): named BEFORE the slot is given back — the lookup's fallback reads this checkout's
3334
+ // branch, and once it is released another run may already be cutting its own here.
3335
+ const deliveredOn = await deliveringBranch();
3336
+ await releaseHold();
3337
+ /**
3338
+ * THE COMPLETION FENCE.
3339
+ *
3340
+ * Every terminal conclusion this pursuit can reach is below: the goal was
3341
+ * met, its work is finished, or nothing here can move without a person.
3342
+ * All three end the pursuit, and none of them may be reached while somebody
3343
+ * still owes an answer.
3344
+ *
3345
+ * WHY EVEN A DENIAL FENCES IT. A refusal that no run has been told about
3346
+ * yet is a fact the goal has not had a chance to plan around. Concluding
3347
+ * "met" or "finished" on the strength of work done BEFORE that answer
3348
+ * existed would close a pursuit on a question it never heard — and the
3349
+ * whole point of resuming is that the answer changes what is worth doing.
3350
+ * Once a resumed run has picked it up, it stops fencing anything.
3351
+ *
3352
+ * READ FRESH, HERE, and not carried from the top of the run. A run lasts
3353
+ * hours; the person may have answered during it, and a conclusion drawn
3354
+ * from a reading taken before the first round would be about a world that
3355
+ * has moved.
3356
+ *
3357
+ * A PLAIN ERROR, AND THAT IS THE LOAD-BEARING PART. Every `RunRefusalCode`
3358
+ * is terminal — `isContinuableRefusal` returns false for all seven — so
3359
+ * refusing here would settle the person's grant because they had not yet
3360
+ * answered a question. Retryable is the truth: the next Start, once they
3361
+ * have, may conclude anything it likes.
3362
+ */
3363
+ if (binding.kind === 'bound' && goalServer !== null) {
3364
+ const fence = await (0, approval_gate_read_1.readApprovalGate)(goalServer.gate, {
3365
+ projectId,
3366
+ goalId: binding.goal.id,
3367
+ goalVersion: binding.goal.version,
3368
+ now: Date.now(),
3369
+ consumed: (ids) => resumes.consumed(ids),
3370
+ });
3371
+ const waiting = (0, core_1.approvalHold)(fence);
3372
+ if (waiting !== null) {
3373
+ log(`action : not calling this goal done — ${waiting}`);
3374
+ await releaseHold();
3375
+ throw new Error(`this goal is waiting on you — ${waiting}`);
3376
+ }
3377
+ }
3378
+ /**
3379
+ * "Nothing to do" was one message and one retryable failure, and it was
3380
+ * hiding three different situations.
3381
+ *
3382
+ * For a goal-bound run it now says which. A goal whose work is FINISHED, or
3383
+ * whose work can no longer move on this machine, will answer the same way
3384
+ * for ever — so a retryable failure buys a start-run offered again every
3385
+ * three seconds until it silently expires an hour later, and the person who
3386
+ * tapped Start is told nothing at all. Terminal is the truthful answer, and
3387
+ * it reaches the phone as a refusal with a reason rather than as silence.
3388
+ *
3389
+ * `working` and `none` stay retryable, because for those the next attempt
3390
+ * really might find something: a draft still to be verified, or a plan not
3391
+ * yet compiled.
3392
+ *
3393
+ * Deliberately NOT a goal state. Nothing is written anywhere — this is
3394
+ * derived from the goal's own slice of the queue, every time it is asked,
3395
+ * so it cannot go stale and there is no new column to keep true.
3396
+ */
3397
+ /**
3398
+ * What the adaptive decision concluded, said in a way that reaches a person.
3399
+ *
3400
+ * These are TERMINAL and produce no run, which is the whole point: the
3401
+ * command is refused with a reason rather than acked `done`, because `done`
3402
+ * on a start-run means a run began and nothing else. The reason travels to
3403
+ * the phone through the transport Phase 6A3 built.
3404
+ */
3405
+ // Retryable, and reported rather than swallowed.
3406
+ //
3407
+ // "Nothing durable was written" would be the tidy thing to say and is not
3408
+ // always true: a throw AFTER `materialiseGoalPlan` leaves the revision on
3409
+ // disk. That is safe — the write is one whole-file rename and the next
3410
+ // Start converges on the same rows — but the message should not claim more
3411
+ // than it knows, so it says what happened rather than what it hopes.
3412
+ if (seamFailure !== null) {
3413
+ await releaseHold();
3414
+ throw new Error(`could not check this goal — ${seamFailure}`);
3415
+ }
3416
+ if (adaptive !== null) {
3417
+ const said = (0, goal_adaptive_1.describeAdaptive)(adaptive, attestedNow(), coverage);
3418
+ // `null` is the compiled-a-plan case, which is not an outcome: the run
3419
+ // went on to work it, and only got here because the plan produced nothing
3420
+ // pickable. The work-set facts below say which nothing that is.
3421
+ if (said !== null) {
3422
+ // The decision's own code travels with it. Nothing here re-derives it.
3423
+ //
3424
+ // 🔴 AND IT RELEASED NOTHING. This exit was invisible to the source pin
3425
+ // that guards the other eleven, because that pin looked for `throw new`
3426
+ // and this is `throw <ternary>`. Widening the pin found it immediately —
3427
+ // which is the argument for structural rules over enumerated ones.
3428
+ // L4b: what key 2 saw, when this is the met-goal refusal and key 2 answered.
3429
+ // Computed BEFORE the release, so the throw stays beside `releaseHold()`.
3430
+ // An ARROW, for the reason `coverageNow` gives: these three are filled
3431
+ // inside a callback, so the straight-line body would narrow them to null.
3432
+ const deliveryNow = () => deliveringRunId !== null && deliveringSha !== null && key2Saw !== null
3433
+ ? { runId: deliveringRunId, sha: deliveringSha, checks: Object.fromEntries(key2Saw.checks) }
3434
+ : undefined;
3435
+ const delivery = said.terminal && said.code === 'goal_satisfied' ? deliveryNow() : undefined;
3436
+ await releaseHold();
3437
+ // The branch the controller will ask the lane about: the one that delivered (F20).
3438
+ const refusedOn = deliveredOn;
3439
+ throw said.terminal ? new RunRefused(said.message, said.code, refusedOn, delivery) : new Error(said.message);
3440
+ }
3441
+ }
3442
+ const left = scope === null ? null : (0, core_1.workLeft)(runQueue.list());
3443
+ if (left === 'finished') {
3444
+ // A WORK-SET fact, not a goal verdict: every row reached a terminal state.
3445
+ // Deliberately NOT `goal_satisfied` — that requires both keys, and this
3446
+ // path never asked the second one.
3447
+ await releaseHold();
3448
+ throw new RunRefused(`this goal's work is done — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`, 'run_refused');
3449
+ }
3450
+ if (left === 'stalled') {
3451
+ await releaseHold();
3452
+ throw new RunRefused(`nothing here can move without you — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`, 'human_required');
3453
+ }
3454
+ await releaseHold();
3455
+ throw new Error(`nothing to do — ${(0, queue_summary_1.describeNoReady)(runQueue.list())}`);
3456
+ }
3457
+ /**
3458
+ * THE APPROVAL BOUNDARY, AND THE ONLY PLACE IT COULD LIVE.
3459
+ *
3460
+ * Everything a request for human authority needs is known here and nowhere
3461
+ * else: which project the server registered, which goal this run is for, at
3462
+ * which version, which run is asking, and — inside the round — which item it
3463
+ * was working. The runner has none of it, the tool bridge has none of it, and
3464
+ * the model must never have any of it.
3465
+ *
3466
+ * `halt` is the one bit of state the run keeps about this. It is armed once,
3467
+ * before the model turn returns to the round protocol, and read by the runner
3468
+ * through a dep that cannot tell an approval from any other reason to stop.
3469
+ *
3470
+ * ─────────────────────────────────────────────────────────────────────────
3471
+ * THE CONTROL PLANE THIS RUN MAY FILE A QUESTION WITH, OR NOTHING.
3472
+ *
3473
+ * Both halves are required and the second is the subtle one. `goalServer` is
3474
+ * built from the machine's own credentials and knows the server's id for this
3475
+ * repository; `projectId` is what this run STAMPS ON ITS EVENTS, and it comes
3476
+ * from the sync. When the sync could not be opened that falls back to
3477
+ * `'local'`, every event this run writes is dropped by ingest on exactly that
3478
+ * mismatch, and the server therefore never learns the run exists — so an
3479
+ * approval naming it could not be validated even though a perfectly good
3480
+ * client is sitting right there. Requiring the two to agree turns a confusing
3481
+ * 400 about an unknown run into a sentence about what is actually wrong.
3482
+ */
3483
+ const approvalServer = goalServer !== null && goalServer.projectId === projectId ? goalServer.approvals : null;
3484
+ const blocking = new run_approval_1.BlockingApproval({
3485
+ server: approvalServer ?? {
3486
+ createApproval: () => Promise.reject(new Error('unreachable: guarded before use')),
3487
+ },
3488
+ // The house rule is that the next cadence is the retry and a tight catch
3489
+ // loop turns an outage into a spin. This is the one place that cannot wait
3490
+ // for a cadence — the run is holding a working tree and is about to end —
3491
+ // so it asks a bounded number of times and pauses between, rather than
3492
+ // firing three refused connections in the same millisecond.
3493
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
3494
+ });
3495
+ /**
3496
+ * Set when the approval could NOT be filed. Thrown after the run has settled,
3497
+ * never instead of settling it: the ledger is the only account of what
3498
+ * happened, and a process that threw before writing one would leave a run
3499
+ * that looks live for ever.
3500
+ */
3501
+ let approvalFailure = null;
3502
+ /**
3503
+ * A model turn asked for something only a person may authorise.
3504
+ *
3505
+ * ORDERING IS THE WHOLE CONTRACT, and it runs downwards: validate what the
3506
+ * model said, build the exact request from trusted context, file it with the
3507
+ * server, and only then arm the halt that ends the run. Every failure above
3508
+ * the last step leaves `halt` unarmed, so a run can never come to rest saying
3509
+ * it is waiting for an approval that does not exist.
3510
+ */
3511
+ const askForApproval = async (declaration, itemId) => {
3512
+ const said = (line) => log(`action : ${line}`);
3513
+ /** Nothing was filed, and the run stops saying so rather than carrying on. */
3514
+ const cannot = (why) => {
3515
+ approvalFailure = why;
3516
+ halt = { stopReason: 'aborted', because: why };
3517
+ said(why);
3518
+ };
3519
+ if (binding.kind !== 'bound') {
3520
+ // Nothing to hang the request on. The server requires a pursuit alongside
3521
+ // a run, and a later controller decides holds by reading approvals for a
3522
+ // goal — one filed without either would be invisible to the thing meant
3523
+ // to act on it, which is worse than not filing it.
3524
+ return cannot('a run with no goal cannot ask for approval');
3525
+ }
3526
+ if (approvalServer === null) {
3527
+ return cannot(goalServer === null
3528
+ ? 'this machine is not paired, so there is nobody to ask'
3529
+ : 'this run is not being mirrored to the server, so nobody could be asked about it');
3530
+ }
3531
+ /**
3532
+ * The model chose the action and its fields. It chose nothing else.
3533
+ *
3534
+ * `runner` is the actor for the same reason every round event carries it:
3535
+ * this happened inside a round, at the machine's own initiative. It is
3536
+ * deliberately not a `human:` actor — a run that could sign as a person
3537
+ * would be a run that could authorise itself — and deliberately not derived
3538
+ * from `opts.autonomous`, because who asked and who said go are different
3539
+ * questions and only the first belongs on the request.
3540
+ */
3541
+ // The project's publish policy travels WITH the request build, so an
3542
+ // action that may not leave this machine is refused before a person is
3543
+ // ever asked about it.
3544
+ const built = (0, pending_action_1.buildPendingActionSignal)(declaration, { projectId, requestedBy: 'runner' }, config.publish.policy);
3545
+ if (!built.ok)
3546
+ return cannot(`that action cannot be requested — ${built.because}`);
3547
+ /**
3548
+ * THE SERVER HAS TO HAVE HEARD OF THIS RUN FIRST.
3549
+ *
3550
+ * Creating the approval names the run it came from, and the server checks
3551
+ * that against a `Run` row it only has because this run's own `run.started`
3552
+ * was ingested. That event is written to the ledger the instant the runner
3553
+ * starts and shipped by the sync on its own cadence — which, for a first
3554
+ * round that finished quickly, may not have come round yet.
3555
+ *
3556
+ * Best-effort on purpose. A flush that fails changes nothing about what
3557
+ * happens next: the create is attempted anyway, and a server that still has
3558
+ * not heard of the run refuses it, which is the honest outcome rather than a
3559
+ * reason not to try.
3560
+ */
3561
+ try {
3562
+ await sync?.daemon.flush();
3563
+ }
3564
+ catch {
3565
+ /* the create below is the real test of whether the server can be reached */
3566
+ }
3567
+ const outcome = await blocking.raise(built.signal, {
3568
+ projectId,
3569
+ goalId: binding.goal.id,
3570
+ goalVersion: binding.goal.version,
3571
+ originRunId: runId,
3572
+ // A REAL identity or none. The round is working this item, so there is
3573
+ // one; nothing here would invent a value to fill the field if there were
3574
+ // not.
3575
+ originItemId: itemId,
3576
+ });
3577
+ if (!outcome.ok)
3578
+ return cannot(`the approval could not be filed — ${outcome.because}`);
3579
+ // DURABLE FIRST, SETTLED SECOND. Past this line the server holds the
3580
+ // question, so a crash before the run finishes ending is recoverable from
3581
+ // the server's own row rather than from anything written here.
3582
+ said(`waiting for you to approve ${built.signal.request.type} — nothing has been done`);
3583
+ halt = {
3584
+ stopReason: 'approval-required',
3585
+ because: 'stopped to ask you to approve something',
3586
+ // Attribution, spread onto `run.ended` by a runner that does not read it.
3587
+ // The id and nothing else: what the approval SAYS is the server's answer
3588
+ // to give, every time it is asked.
3589
+ endedContext: { approvalId: outcome.approvalId },
3590
+ };
3591
+ };
3592
+ const deps = {
3593
+ ledger: ledgerForRun,
3594
+ queue: runQueue,
3595
+ git,
3596
+ // Passed straight through: an item's cross-repo needs are decided by
3597
+ // `pickableItems`, and the only thing this file contributes is the wire.
3598
+ ...(opts.crossRepo === undefined ? {} : { crossRepo: opts.crossRepo }),
3599
+ // CONTAINED. A gate step is the project's own tests over code the agent
3600
+ // just wrote, so it runs inside the boundary with Looply's own runtime
3601
+ // rather than as the daemon with the machine's.
3602
+ gateRunner: (0, untrusted_gate_1.makeUntrustedGateRunner)(cwd),
3603
+ // ASKED EVERY ROUND, NEVER CACHED. ~0.6 s against a 45-minute agent
3604
+ // ceiling, and asking each time is what makes the three-round bound mean
3605
+ // anything — and what lets a machine that recovers mid-run carry on.
3606
+ environmentReady: (0, untrusted_gate_1.makeContainmentPreflight)(cwd),
3607
+ implement: async (item, attempt, repair) => {
3608
+ roundNo += 1;
3609
+ terminal.phase(roundNo, 'implementing', item.title);
3610
+ /**
3611
+ * 🔴 ONE SNAPSHOT FOR THIS ROUND, TAKEN HERE.
3612
+ *
3613
+ * Asked per round rather than once per run, because a previous round
3614
+ * may have committed — and asked ONCE per round rather than twice,
3615
+ * because the memory ranking and the structural block must describe the
3616
+ * same world. Two calls either side of a commit would rank memories
3617
+ * against one tree and print a neighbourhood from another, and nothing
3618
+ * downstream could tell.
3619
+ */
3620
+ const graphNow = await currentGraph();
3621
+ const goalNow = binding.kind === 'bound' ? binding.goal.id : undefined;
3622
+ const turn = await (0, agent_driver_1.runImplementer)(agent, {
3623
+ itemId: item.id,
3624
+ // The decision travels to the model here and nowhere else. It is not on
3625
+ // the item, not in the queue, and not a runner dep — the round protocol
3626
+ // has no vocabulary for it and must not acquire one.
3627
+ prompt: buildImplementerPrompt(item, attempt, config, resolution, governed,
3628
+ // Scoped to the item's own subject as well as the pursuit: what failed
3629
+ // on THIS piece of work is what a round most needs not to repeat.
3630
+ await memoryFor(memoryStore, git, { goalId: goalNow }, graphNow),
3631
+ /**
3632
+ * 🔴 THE GRAPH REACHING A PROMPT. Same snapshot as the line above, so
3633
+ * the structure a model is shown and the memories it is ranked by
3634
+ * describe ONE world — two calls could straddle a commit and disagree.
3635
+ */
3636
+ structureFor(graphNow, goalNow),
3637
+ // What the last attempt did and why it was refused, when this run
3638
+ // saw it refused. The runner built it from the retained evidence.
3639
+ repair,
3640
+ /**
3641
+ * 🔴 AND THE GOAL ITSELF, WHICH WAS ALWAYS ONE PROPERTY AWAY. The
3642
+ * line that computes `goalNow` reads `binding.goal.id` off this same
3643
+ * object; `binding.goal.text` is the person's own wording, preserved
3644
+ * verbatim, and it was being left behind on every round of every
3645
+ * pursuit (the audit's E-01). Read from the binding rather than
3646
+ * fetched, so the text a round is shown is the text the round is
3647
+ * bound to — a second read could straddle an amendment and show a
3648
+ * goal this work was never authorised against.
3649
+ */
3650
+ binding.kind === 'bound' ? binding.goal.text : undefined),
3651
+ timeoutMs: AGENT_TIMEOUT_MS,
3652
+ });
3653
+ if (turn.kind !== 'pending-action')
3654
+ return turn;
3655
+ // NARROWED HERE, ONCE. A request for authority is not something a round
3656
+ // can be told about, so it never reaches the runner as one: the boundary
3657
+ // is dealt with, the halt is armed, and what goes back is a turn that
3658
+ // produced no implementable result — which is exactly what happened.
3659
+ await askForApproval(turn.declaration, item.id);
3660
+ const { transcript, ms, exitOk, timedOut } = turn;
3661
+ return {
3662
+ kind: 'abandoned',
3663
+ reason: halt?.because ?? 'the turn asked for an action instead of implementing',
3664
+ transcript,
3665
+ ms,
3666
+ exitOk,
3667
+ timedOut,
3668
+ };
3669
+ },
3670
+ ...(config.proof.enabled
3671
+ ? {
3672
+ prove: async ({ item }) => {
3673
+ terminal.phase(roundNo, 'proving', item.title);
3674
+ const diff = await git.diff();
3675
+ const run = await (0, prover_1.runProver)(proveAgent, {
3676
+ item: { title: item.title, evidence: item.evidence },
3677
+ diff,
3678
+ // Held only so the exclusion is enforced where it is testable;
3679
+ // the prompt builder never reads it. The prover grades the
3680
+ // code, not the implementer's account of it.
3681
+ implementer: {
3682
+ itemId: item.id,
3683
+ outcome: 'implemented',
3684
+ filesTouched: [],
3685
+ testsAdded: 0,
3686
+ note: '',
3687
+ blocked: null,
3688
+ },
3689
+ }, PROVER_TIMEOUT_MS);
3690
+ return (0, prover_1.toProofOutcome)(run);
3691
+ },
3692
+ /**
3693
+ * 🔴 §6.C, AND THE LINE `config.ts` HAS BEEN WARNING ABOUT SINCE
3694
+ * M6-20. `on_partial: ask` is the DEFAULT and nothing could ask, so a
3695
+ * partial proof was discarded with a log line while the server held
3696
+ * the whole mechanism and the phone rendered it.
3697
+ *
3698
+ * ⚠️ AN UNPAIRED MACHINE IS UNCHANGED. `goalServer` is null without a
3699
+ * paired machine and a registered project, and the asker says the
3700
+ * same two lines and discards, exactly as this did.
3701
+ */
3702
+ resolvePartial: (0, partial_asker_1.makePartialAsker)({
3703
+ /**
3704
+ * 🔴 ONE FIELD, SO THERE IS NOTHING TO THROW ABOUT. Unpaired is the
3705
+ * ABSENCE of a server, not a null id beside calls that need one —
3706
+ * and a bare `throw` in this file is how a run leaks its lease and
3707
+ * its slot, which `run-hold-release.spec.ts` says out loud.
3708
+ */
3709
+ server: goalServer === null
3710
+ ? null
3711
+ : {
3712
+ projectId: goalServer.projectId,
3713
+ ask: (body) => goalServer.questions.ask(body),
3714
+ read: (p, id) => goalServer.questions.read(p, id),
3715
+ },
3716
+ runId,
3717
+ now: () => Date.now(),
3718
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
3719
+ windowMs: config.proof.askWindowMin * 60_000,
3720
+ graceMs: partial_asker_1.PARTIAL_ASK_GRACE_MS,
3721
+ pollMs: partial_asker_1.PARTIAL_ASK_POLL_MS,
3722
+ log,
3723
+ }),
3724
+ }
3725
+ : {}),
3726
+ evidence: { invalidate: (itemId) => evidence.invalidate(itemId) },
3727
+ // Where a rejected candidate goes before `git reset --hard` takes the tree:
3728
+ // under the state directory, which `discard()` spares by name.
3729
+ rejectedEvidence: (0, rejected_evidence_1.makeRejectedEvidenceStore)(cwd),
3730
+ reverify: {
3731
+ cache: {
3732
+ lookup: (itemId, head) => evidence.lookup(itemId, head),
3733
+ put: (itemId, head, ev) => evidence.put(itemId, head, ev),
3734
+ },
3735
+ countCommitsTouching: (from, to, paths) => git.countCommitsTouching(from, to, paths),
3736
+ // Same verifier the pre-run pass uses, so an item's verdict never
3737
+ // depends on which route asked for it.
3738
+ verify: (0, verify_cmd_1.makeItemVerifier)(verifyAgent, queue),
3739
+ },
3740
+ notifier,
3741
+ readEnvFile: (p) => {
3742
+ try {
3743
+ return (0, node_fs_1.readFileSync)((0, node_path_1.join)(cwd, p), 'utf8');
3744
+ }
3745
+ catch {
3746
+ return null;
3747
+ }
3748
+ },
3749
+ env: process.env,
3750
+ now: () => Date.now(),
3751
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
3752
+ stopRequested: () => signals.requested(),
3753
+ // The generic half of the boundary above. The runner asks whether the rest
3754
+ // of this run is still possible and is handed a reason; it never learns
3755
+ // that the reason is a person.
3756
+ haltRequested: () => halt,
3757
+ /**
3758
+ * ⚠️ ONLY WHEN THE PROJECT ASKED, and only for a run that has a base to be
3759
+ * stale. With `run.integration` off there is no `looply/integration` ref,
3760
+ * so this is not wired at all rather than wired and answering "current".
3761
+ */
3762
+ ...(config.run.integration === true
3763
+ ? {
3764
+ /**
3765
+ * 🔴 `.catch(() => ({ kind: 'current' }))` WAS THE THIRD FAIL-OPEN IN
3766
+ * ONE CHECK, AND THE MOST DIRECT.
3767
+ *
3768
+ * `current` is this function's word for "the ground has not moved,
3769
+ * carry on". So a broken git, a missing base commit, a repository
3770
+ * mid-`rebase` with an index lock — every one of them told the run
3771
+ * that its base was fresh, which is the single answer this check
3772
+ * exists to be sure about. Combined with the two inside git-driver, a
3773
+ * staleness check failed open in three independent ways.
3774
+ *
3775
+ * A staleness check that cannot check has to HALT. That is the same
3776
+ * rule the loop-safety history follows ("an unreadable history must
3777
+ * hold, not pass") and the same one `withinEnvelope` follows for an
3778
+ * unmeasurable ceiling. Stopping costs a run; carrying on builds an
3779
+ * hour of work on ground that moved.
3780
+ */
3781
+ /**
3782
+ * WHERE THIS RUN'S BRANCH IS CUT FROM — the other half of the phase.
3783
+ *
3784
+ * ⚠️ `cutFrom` ANSWERS null UNTIL INTEGRATION EXISTS, and the runner
3785
+ * then cuts from HEAD exactly as it always has. Once it exists, every
3786
+ * run starts from the same base instead of stacking on the last one.
3787
+ */
3788
+ integrationBase: async () => {
3789
+ try {
3790
+ const at = await (0, integration_1.cutFrom)(git, true);
3791
+ return at === null ? null : at.ref;
3792
+ }
3793
+ catch {
3794
+ // A start point that cannot be read is not a reason to refuse the
3795
+ // run; it is a reason to start where runs have always started.
3796
+ return null;
3797
+ }
3798
+ },
3799
+ baseStale: async (cutAt) => {
3800
+ try {
3801
+ // F29: the commit the runner CUT from — never this checkout's HEAD before the cut, which in
3802
+ // a reused checkout is the LAST run's tip and read every moved lane as a stale base (corr-7).
3803
+ const out = await (0, integration_1.baseFreshness)(git, cutAt, true);
3804
+ /**
3805
+ * ⚠️ UNINITIALISED STOPS THE RUN TOO, and does not read as fresh.
3806
+ * `run.integration` is on, so this project asked for its runs to
3807
+ * converge; with no integration ref there is nothing to converge
3808
+ * on and nothing to compare against. Carrying on while reporting
3809
+ * freshness is the one answer this check exists to prevent.
3810
+ */
3811
+ return out.kind === 'current' ? null : out.because;
3812
+ }
3813
+ catch (e) {
3814
+ return (`this run cannot tell whether its base is still current (${e.message}). ` +
3815
+ 'Refusing to build on ground it cannot check');
3816
+ }
3817
+ },
3818
+ }
3819
+ : {}),
3820
+ // Attribution for this run's header event, passed as opaque values. The
3821
+ // runner spreads them and knows nothing about what they mean — that is the
3822
+ // whole point of the carrier, and why RoundRunner still contains no
3823
+ // occurrence of the word.
3824
+ ...(binding.kind === 'bound'
3825
+ ? {
3826
+ runContext: {
3827
+ goalId: binding.goal.id,
3828
+ goalVersion: binding.goal.version,
3829
+ // Which answer this run was started to pick up. Lineage, carried on
3830
+ // the same opaque channel the goal binding uses — the runner spreads
3831
+ // it onto the header and knows nothing about what it means.
3832
+ ...(resolution === undefined ? {} : { resumedFromApprovalId: resolution.approvalId }),
3833
+ },
3834
+ }
3835
+ : {}),
3836
+ // The phone's pause and stop. The daemon writes this file from another
3837
+ // process; the runner reads it between rounds.
3838
+ control,
3839
+ };
3840
+ /**
3841
+ * THE GRANTS A PERSON HAS OPENED, AND THE RECORD OF WHAT THIS MACHINE SPENT.
3842
+ *
3843
+ * 🔴 THIS LINK DID NOT EXIST, AND EVERYTHING EITHER SIDE OF IT DID. The gate's
3844
+ * validity check, the core model, the server's grant and list routes and the
3845
+ * commit trailer line were all built and tested around a door nothing could
3846
+ * open: `looply run` wired neither a source of grants nor a sink for spent
3847
+ * ones, so no grant reached a gate from a run and a railed path stayed shut
3848
+ * whatever a person granted.
3849
+ *
3850
+ * ⚠️ BOTH OR NEITHER, AND THE RUNNER ENFORCES THAT. Its constructor refuses a
3851
+ * grant source with no spend sink — "one grant reopening the same rail every
3852
+ * round until it expires" — so this cannot be half-wired into a door that
3853
+ * never closes.
3854
+ *
3855
+ * ⚠️ NO SERVER, NO GRANTS. Without `sync` there is no trusted source, and the
3856
+ * runner then keeps every rail shut. That is the same deny-by-default the
3857
+ * whole design rests on, and it is why a failure to fetch below yields an
3858
+ * EMPTY list rather than an exception that would strand the run.
3859
+ */
3860
+ // MACHINE-SCOPED. "Single use" held once only because there was one
3861
+ // exemptions ledger per checkout; one per worktree let the same granted row
3862
+ // pass every filter in each of them, and a single-use grant over a railed
3863
+ // path became spendable once per workspace. Phase 1 closed that by sharing
3864
+ // the project's ledger. Phase 2 moved it out of the repository, because a
3865
+ // ledger a person can delete by re-cloning is a ledger that forgets what the
3866
+ // machine has spent. See rails-home.ts.
3867
+ const exemptionStore = (0, rails_stores_1.railsExemptionStore)(cwd);
3868
+ let grants = [];
3869
+ if (sync) {
3870
+ await holding(() => {
3871
+ faulting('exemptionStore.open');
3872
+ return exemptionStore.open();
3873
+ });
3874
+ try {
3875
+ const { exemptions: rows } = await sync.client.projectExemptions(sync.projectId);
3876
+ // The three filters live in `grantsFromServer`, where they can be tested
3877
+ // and mutated one at a time: granted-not-refused, a real expiry, and not
3878
+ // already spent on this machine.
3879
+ grants = (0, exemption_store_1.grantsFromServer)(rows, (e) => exemptionStore.isSpent(e), (g) => (0, core_1.exemptionFromGrant)({ ...g, actor: g.actor }));
3880
+ if (grants.length > 0)
3881
+ log(`rails : ${grants.length} granted exemption(s) available`);
3882
+ }
3883
+ catch {
3884
+ // A grant source that cannot be reached is no grant source. Rails shut.
3885
+ grants = [];
3886
+ }
3887
+ }
3888
+ const runner = new round_runner_1.RoundRunner({ ...config, run: { ...config.run, minHours } }, {
3889
+ ...deps,
3890
+ // ORIGIN TRANSLATED ONCE, HERE, into a policy the runner can act on
3891
+ // without knowing what a controller is. A person's tap consumes their
3892
+ // own control request; a Start this machine chose to make does not get
3893
+ // to overrule a person's pause with an act they never performed.
3894
+ consumeControlOnStart: opts.autonomous !== true,
3895
+ /**
3896
+ * ⚠️ ONLY WHEN A PURSUIT COULD BE OBSERVED AT ALL. Collecting per-round
3897
+ * facts for an unbound run costs a `headTreeSha` per round and produces
3898
+ * records nothing will ever read: the producer below is guarded on
3899
+ * `binding.kind === 'bound'`.
3900
+ */
3901
+ perRoundSafety: config.run.perRoundSafety === true && binding.kind === 'bound',
3902
+ /**
3903
+ * ⚠️ ONLY WHEN THE PROJECT ASKED. With `run.surfaces` off this is empty,
3904
+ * and an empty declaration constrains nothing — which is the whole
3905
+ * compatibility guarantee, made by the same line that would enforce it.
3906
+ */
3907
+ ...(heldSurfaces.length === 0 ? {} : { declaredSurfaces: declaredSurfaces }),
3908
+ /**
3909
+ * 🔴 THE FENCE, WIRED. `checkFence` shipped with no production caller at
3910
+ * all — called only by its own unit test — so `surface-lease.ts`'s claim
3911
+ * that "EXPIRY EXISTS ONLY HERE, AND ONLY BECAUSE OF THE FENCE" was half
3912
+ * true: the expiry shipped and the comparison that makes expiry safe did
3913
+ * not. The runner asks this immediately before every commit.
3914
+ *
3915
+ * ⚠️ EVERY HELD SURFACE, AND THE FIRST REFUSAL WINS. A run holding three
3916
+ * surfaces that has been taken over on one of them is not the current
3917
+ * holder, and committing any part of that round would be the very
3918
+ * two-writer state the lease exists to prevent.
3919
+ */
3920
+ ...(heldSurfaces.length === 0
3921
+ ? {}
3922
+ : {
3923
+ checkFence: () => {
3924
+ for (const fence of heldSurfaces) {
3925
+ const verdict = (0, surface_lease_1.checkFence)(cwd, fence);
3926
+ if (!verdict.ok)
3927
+ return verdict;
3928
+ }
3929
+ return { ok: true };
3930
+ },
3931
+ }),
3932
+ /**
3933
+ * 🔴 THE ONE RETRY IS SPENT WHEN A ROUND IS CERTAIN, NOT BEFORE.
3934
+ *
3935
+ * MEASURED, twice, on the version that spent it at the statement before
3936
+ * `runner.run()`: the dirty-tree check is `run()`'s FIRST statement, so a
3937
+ * person with uncommitted edits saw "one retry, spent" and "working tree
3938
+ * has 1 changed path(s) — refusing to start" in the same second, with no
3939
+ * ledger written and no round begun. The hold was discharged for ever and
3940
+ * the next unflagged `looply run` walked straight past the fence. The
3941
+ * clean-tree variant reached the same end through `queue-empty`.
3942
+ *
3943
+ * The runner calls this after its own preconditions and after an item is
3944
+ * picked, which is the first moment a round is certain.
3945
+ */
3946
+ ...(retryToSpend === null || safety === null
3947
+ ? {}
3948
+ : {
3949
+ beforeFirstRound: async () => {
3950
+ const spent = await safety.override(retryToSpend.id, runId);
3951
+ if (!spent.ok) {
3952
+ return { ok: false, because: `${retryToSpend.because}; ${spent.because}. That retry is already gone.` };
3953
+ }
3954
+ log(`action : trying once more after ${retryToSpend.kind} (${retryToSpend.id}) — one retry, spent`);
3955
+ return { ok: true };
3956
+ },
3957
+ }),
3958
+ // Wired as a PAIR, because the runner refuses one without the other.
3959
+ // Without a server there is nothing to wire and every rail stays shut.
3960
+ ...(sync
3961
+ ? {
3962
+ // Item-scoped here; path, expiry and unused-ness are judged at the
3963
+ // interlock by `isExemptionValid`, which is the only place that
3964
+ // sees the path being opened.
3965
+ // 🔴 ASKS THE STORE EVERY ROUND, not just at fetch time. Filtering
3966
+ // the captured array alone kept offering a grant that an earlier
3967
+ // round of THIS run had already spent — "one grant reopening the
3968
+ // same rail every round until it expires", which is the exact
3969
+ // failure the constructor below refuses a half-wiring to prevent.
3970
+ // Wiring both halves satisfied that check and did it anyway.
3971
+ exemptionsFor: (itemId) => (0, exemption_store_1.offerableFor)(grants, itemId, (e) => exemptionStore.isSpent(e)),
3972
+ // Spent on COMMIT, not on check: a discarded round leaves the door
3973
+ // open for the retry. The runner calls this after the commit exists.
3974
+ spendExemption: async (spent) => {
3975
+ await exemptionStore.record(spent);
3976
+ },
3977
+ }
3978
+ : {}),
3979
+ }, identity);
3980
+ // The branch is NOT created here. RoundRunner.run() creates it, and
3981
+ // GitDriver.createBranch refuses a name that already exists (DESIGN section
3982
+ // 12 case 2 — reusing a branch would land this run's commits on top of
3983
+ // whatever an aborted run left there). Creating it in both places made that
3984
+ // refusal fire against the run's own branch, so every run exited 1 after
3985
+ // printing these three lines and no round was ever reached. The runner owns
3986
+ // git; this file only reports what the runner is about to do.
3987
+ log(`branch : ${branch}`);
3988
+ log(`base : ${baseSha.slice(0, 7)}`);
3989
+ log(`budget : ${minHours}h, up to ${config.run.maxRounds} rounds`);
3990
+ /**
3991
+ * THE RUN-END DELIVERY (F20), ON EVERY WAY OUT THAT COMMITTED (F31, corr-12). A run that
3992
+ * committed and then threw out of the runner left its branch with no claim, and the lane holds an
3993
+ * unclaimed branch to the strict rule — refused while any capability step is red, and not offered
3994
+ * again until the base moves. On a throw the summary is lost, so `onlyIfCommitted` asks this
3995
+ * run's own rounds instead.
3996
+ */
3997
+ const writeRunEndDelivery = async (onlyIfCommitted) => {
3998
+ const at = scope;
3999
+ if (at === null)
4000
+ return;
4001
+ try {
4002
+ const read = await (0, round_records_1.readRoundRecords)(stateDir, ledger_store_1.nodeLedgerFs);
4003
+ if (onlyIfCommitted && (!read.ok || !read.rounds.some((r) => r.runId === runId && r.outcome === 'committed')))
4004
+ return;
4005
+ const head = await git.headSha();
4006
+ const mineIds = new Set(queue.list().filter((i) => i.goalId === at.goalId).map((i) => i.id));
4007
+ const ours = read.ok ? read.rounds.filter((r) => mineIds.has(r.itemId)) : [];
4008
+ const end = read.ok
4009
+ ? (0, run_delivery_1.deliveryAtRunEnd)({
4010
+ scope: at,
4011
+ items: queue.list(),
4012
+ rounds: read.rounds,
4013
+ head,
4014
+ requiredSteps: (0, gate_1.requiredSteps)(config.gates),
4015
+ branch,
4016
+ runId,
4017
+ acceptanceDigest: (0, core_3.acceptanceMapDigest)(config.gates),
4018
+ at: new Date().toISOString(),
4019
+ // F31 (soun-4): only work this branch's history contains.
4020
+ reachable: await (0, run_delivery_1.reachableFrom)(git, head, ours),
4021
+ })
4022
+ : { kind: 'none', because: read.because };
4023
+ if (end.kind === 'record') {
4024
+ if (!(0, lane_claims_1.recordLaneClaim)(cwd, end.record))
4025
+ log('delivery: could not be written — the lane will hold this branch to the strict rule');
4026
+ }
4027
+ else {
4028
+ log(`delivery: none recorded for the lane — ${end.because}`);
4029
+ }
4030
+ }
4031
+ catch (e) {
4032
+ log(`delivery: none recorded for the lane — ${e.message}`);
4033
+ }
4034
+ };
4035
+ try {
4036
+ const summary = await runner.run().catch(async (e) => {
4037
+ await writeRunEndDelivery(true);
4038
+ throw e;
4039
+ });
4040
+ /**
4041
+ * 🔴 THE RUN DID NOTHING, AND SAYS SO RATHER THAN LOOKING LIKE A RUN.
4042
+ *
4043
+ * `beforeFirstRound` refuses when the one retry could not be taken. The
4044
+ * summary carries the reason so the ending is honest — this is not a run
4045
+ * that finished, it is a run that never started.
4046
+ */
4047
+ /**
4048
+ * 🔴 A ROUND THAT WROTE OUTSIDE ITS LEASE FLAGS THE GOAL AND IS NOT RETRIED.
4049
+ *
4050
+ * The round was already discarded where the trespass was seen — before the
4051
+ * gate, before any commit. What is left is to say so, with the exact paths,
4052
+ * because the person's DECLARATION is what needs changing and they cannot
4053
+ * change it from "something went wrong".
4054
+ */
4055
+ if (summary.surfaceViolation !== undefined) {
4056
+ const v = summary.surfaceViolation;
4057
+ log(`surface : wrote outside ${v.declared.join(', ')} — ${v.paths.slice(0, 5).join(', ')}`);
4058
+ log('surface : the round was discarded and this goal is flagged; the declaration is what is wrong');
4059
+ }
4060
+ if (summary.refusedBeforeFirstRound !== undefined) {
4061
+ log(`action : not starting work — ${summary.refusedBeforeFirstRound}`);
4062
+ await releaseHold();
4063
+ throw new Error(`this machine has stopped working on that goal — ${summary.refusedBeforeFirstRound}`);
4064
+ }
4065
+ /**
4066
+ * 🔴 THE DELIVERY IS WRITTEN BY THE RUN THAT DELIVERED IT, ON THE BRANCH IT CUT,
4067
+ * BEFORE ITS SLOT IS GIVEN BACK (F20). The daemon offers a finished branch to the
4068
+ * lane as soon as the slot is free, and under programme admission a branch the
4069
+ * lane finds no claim for gets the strict rule and is not offered again at that
4070
+ * tip. It used to be written only by a LATER Start, under that Start's own
4071
+ * never-created branch — so the lane never found it at all.
4072
+ *
4073
+ * ⚠️ KEY 2's OWN ANSWER (`deliveryAtRunEnd`), and best-effort: a delivery that
4074
+ * could not be written leaves the branch to the strict lane, never to a crash.
4075
+ */
4076
+ if (scope !== null && summary.totals.committed > 0)
4077
+ await writeRunEndDelivery(false);
4078
+ /**
4079
+ * WHAT THIS RUN ACHIEVED, WRITTEN DOWN BEFORE ANYTHING ELSE CAN THROW.
4080
+ *
4081
+ * Recorded here rather than at the very end because the paths below throw,
4082
+ * and an attempt that is not written down is an attempt the detectors
4083
+ * cannot see — a run that keeps failing the same way would be invisible to
4084
+ * the one mechanism built to notice.
4085
+ *
4086
+ * ⚠️ THE EXCLUSION IS DERIVED FROM THE STOP REASON, NOT GUESSED. A run that
4087
+ * stopped because a person was asked something has not failed to progress;
4088
+ * it stopped ON PURPOSE, and counting it would turn somebody's ten minutes
4089
+ * over an approval into a safety incident. `approval-required` and the
4090
+ * ambiguous-action reason are the two the runner can end on that mean
4091
+ * "waiting for a person", and both are excluded.
4092
+ *
4093
+ * Bound pursuits only: an unbound run has no pursuit whose progress could
4094
+ * be measured, and inventing one would be inventing the very number this
4095
+ * phase refuses to invent.
4096
+ */
4097
+ if (binding.kind === 'bound' && safety !== null && scope !== null) {
4098
+ /**
4099
+ * ⚠️ NOT DERIVABLE FROM `stopReason`, which is why the runner carries it
4100
+ * out separately: an environment-blocked run ends as `aborted`, and so
4101
+ * does an ordinary crash-out. Without this the run is recorded as
4102
+ * `gate_failed` — the outcome list's words for "a deterministic gate
4103
+ * refused it", which is exactly what did not happen — and its unchanged
4104
+ * tree fingerprint feeds `noProgress`, which needs no failure signature
4105
+ * and so survived the whole point of the `blocked` verdict.
4106
+ *
4107
+ * ⚠️ AND PAIRED WITH `committed === 0`, deliberately. Meeting a broken
4108
+ * container is not by itself a reason to record nothing: a run that hit
4109
+ * one, recovered and landed work made real progress, and hiding that
4110
+ * would be the opposite error — a pursuit that IS stuck looking healthy.
4111
+ */
4112
+ const excluded = summary.stopReason === 'approval-required'
4113
+ ? 'awaiting_approval'
4114
+ : summary.stopReason === 'governed-action-ambiguous'
4115
+ ? 'awaiting_human_resolution'
4116
+ : summary.metUnavailableEnvironment === true && summary.totals.committed === 0
4117
+ ? 'environment_unavailable'
4118
+ : null;
4119
+ /**
4120
+ * ONE OBSERVATION PER ROUND, OR — STILL, BY DEFAULT — ONE PER RUN.
4121
+ *
4122
+ * 🔴 THE DENOMINATOR WAS THE DEFECT. A four-hour run that discarded forty
4123
+ * rounds on the identical failure was ONE observation, so
4124
+ * `MAX_SAME_FAILURE_REPEATS = 3` needed three such RUNS — half a day —
4125
+ * before it said anything, while `MAX_ITEM_EXECUTION_ATTEMPTS = 3` was
4126
+ * being spent per round. The two constants read as if they agreed and
4127
+ * were measuring different things by a factor of the round count.
4128
+ *
4129
+ * ⚠️ THE OBSERVATIONS STILL ARRIVE AT THE END OF THE RUN, and that is a
4130
+ * deliberate narrowing of what the plan sketched. Recording them AS each
4131
+ * round ends would make a hold reachable inside a single Mode-1 run —
4132
+ * three consecutive rounds failing on one flaky test is an ordinary
4133
+ * evening, not a pathology — and a hold has no expiry. What this fixes is
4134
+ * what the NEXT run's brake is told, which is the defect that was
4135
+ * actually costing people money.
4136
+ *
4137
+ * ⚠️ AND `excluded` IS RE-DERIVED PER ROUND for the environment case,
4138
+ * because `metUnavailableEnvironment` latches true for the rest of the
4139
+ * run: reading it directly would exclude every round after the first bad
4140
+ * container, including the ones that worked.
4141
+ */
4142
+ const perRound = summary.rounds ?? null;
4143
+ const records = perRound === null
4144
+ ? [
4145
+ {
4146
+ worked: summary.totals.committed > 0,
4147
+ /**
4148
+ * 🔴 IT USED TO SAY `null` HERE, AND null MAPS TO `gate_failed`.
4149
+ *
4150
+ * The old comment argued that a run summarising many rounds
4151
+ * cannot say where it failed — which is true, and is not an
4152
+ * argument for naming the GATE. A prover refusal recorded as a
4153
+ * gate failure is a specific false claim, and it was the
4154
+ * claim made in the DEFAULT configuration: per-round facts
4155
+ * exist only when `run.per_round_safety` is on, and it is off
4156
+ * by default.
4157
+ *
4158
+ * The runner maintains `failedAtThisRound` on every round
4159
+ * whatever that flag says — only the facts ARRAY is gated — so
4160
+ * the last unsuccessful round's site is knowable here for free.
4161
+ * It is the LAST one, not a claim about all of them.
4162
+ *
4163
+ * ⚠️ AND UNDEFINED STILL MEANS `gate_failed`, unchanged: a run
4164
+ * that never set it never failed a round in a way the runner
4165
+ * classified.
4166
+ */
4167
+ failedAt: summary.lastFailedAt ?? null,
4168
+ // See `failedAt` above: a run summarises many rounds and cannot
4169
+ // say which gates passed in which of them.
4170
+ passingGates: undefined,
4171
+ tree: await git.headTreeSha().catch(() => null),
4172
+ failure: summary.lastFailure ?? null,
4173
+ work: runQueue.list(),
4174
+ excludedFor: excluded,
4175
+ },
4176
+ ]
4177
+ : perRound.map((r) => ({
4178
+ worked: r.worked,
4179
+ failedAt: r.failedAt,
4180
+ passingGates: r.passingGates,
4181
+ tree: r.treeFingerprint,
4182
+ failure: r.failure,
4183
+ /**
4184
+ * 🔴 THE ROUND'S OWN WORK LIST, NOT THE RUN'S FINAL ONE.
4185
+ *
4186
+ * Every record used to be built from ONE snapshot taken after the
4187
+ * run ended, so all N carried the same progress fingerprint — and
4188
+ * `noProgress` compares those for equality and never looks at the
4189
+ * outcome. Three rounds was therefore enough, unconditionally: a
4190
+ * run that picked three items, gated them green and committed all
4191
+ * three recorded that it had achieved nothing three times, and
4192
+ * held its own goal. Measured.
4193
+ */
4194
+ work: r.work,
4195
+ excludedFor: r.metUnavailableEnvironment && !r.worked ? 'environment_unavailable' : excluded,
4196
+ }));
4197
+ /**
4198
+ * 🔴 THE HIGH-WATER MARK OF GATES THAT HAVE PASSED, ACROSS THIS RUN.
4199
+ *
4200
+ * `passingGates` feeds the progress fingerprint, and `noProgress` fires
4201
+ * only on three IDENTICAL fingerprints in a row. Fed the raw per-round
4202
+ * set, one flaky step — a timeout, a port in use, a slow container —
4203
+ * changes the set every round, so three identical fingerprints never
4204
+ * happen and ALL FOUR detectors are off for a pursuit that is stuck.
4205
+ *
4206
+ * A union only ever grows: a step passing for the first time is progress,
4207
+ * a step flapping is not. Seeded empty per run, which is the honest bound
4208
+ * — the observations are durable but their vectors are not, so this is a
4209
+ * within-run high-water mark and is described as one.
4210
+ */
4211
+ const gatesEverPassed = new Set();
4212
+ for (const rec of records) {
4213
+ /**
4214
+ * ⚠️ AN EXCLUDED ATTEMPT MINTS NOTHING. `observationFor` returns null for
4215
+ * one, and the mint used to happen as an argument to it — so the claim
4216
+ * file was written and the observation thrown away. Those claims
4217
+ * accumulate above the last observation and are never reclaimed, which is
4218
+ * how the mint's candidate window came to be burnt through. Asking the
4219
+ * question first costs a comparison.
4220
+ */
4221
+ if (!(0, core_4.countsTowardSafety)(rec.excludedFor))
4222
+ continue;
4223
+ const observation = (0, loop_safety_observer_1.observationFor)({
4224
+ goalId: binding.goal.id,
4225
+ goalVersion: binding.goal.version,
4226
+ // Not parsed out of a request id — this repository refuses to derive
4227
+ // anything from one. It counts on from the last attempt rather than
4228
+ // counting the history, which saturates once the history is trimmed.
4229
+ ordinal: await safety.mintOrdinal(binding.goal.id),
4230
+ runId,
4231
+ /**
4232
+ * 🔴 FOUR OUTCOMES, NOT TWO. This was
4233
+ * `committed > 0 ? 'worked' : 'gate_failed'`, so `verifier_failed` and
4234
+ * `action_rejected` had no production producer at all — a four-value
4235
+ * enum with two unreachable values, making every observation coarser
4236
+ * than it looked. A round the PROVER refused was recorded as "a
4237
+ * deterministic gate refused it", which is a different fact about a
4238
+ * different mechanism.
4239
+ *
4240
+ * ⚠️ AND AN UNCLASSIFIED FAILURE IS STILL `gate_failed`, which is what
4241
+ * the run-level path has always produced — so the default behaviour is
4242
+ * unchanged and only the newly-distinguishable cases move.
4243
+ */
4244
+ outcome: rec.worked
4245
+ ? 'worked'
4246
+ : rec.failedAt === 'verifier'
4247
+ ? 'verifier_failed'
4248
+ : rec.failedAt === 'action'
4249
+ ? 'action_rejected'
4250
+ : 'gate_failed',
4251
+ pursuit: {
4252
+ items: rec.work,
4253
+ planRevision: (0, goal_ownership_1.currentPlanRevision)(rec.work, binding.goal.id, binding.goal.version),
4254
+ },
4255
+ ...(rec.passingGates === undefined ? {} : { passingGates: rec.passingGates }),
4256
+ // Everything that had already passed before this round. See the set's
4257
+ // declaration above: the union is what makes the contribution monotone.
4258
+ gatesEverPassed: [...gatesEverPassed],
4259
+ treeFingerprint: rec.tree,
4260
+ /**
4261
+ * 🔴 THIS WAS A LITERAL `null` AND SO `repeated_failure` COULD NEVER
4262
+ * FIRE — a whole detector, fully tested against hand-built inputs,
4263
+ * unreachable because the one producer never fed it. Found by an
4264
+ * adversarial review, not by any gate.
4265
+ *
4266
+ * Signed only when the run actually hit a deterministic failure.
4267
+ * Absent means "none", which is different from "one nobody could
4268
+ * identify" — the latter arrives as a real signature with a null digest.
4269
+ */
4270
+ failureSignature: rec.failure === null ? null : (0, core_1.failureSignature)(rec.failure),
4271
+ actionFingerprint: null,
4272
+ excluded: rec.excludedFor,
4273
+ at: new Date().toISOString(),
4274
+ });
4275
+ if (observation !== null) {
4276
+ const verdict = await safety.observe(observation);
4277
+ if (verdict.kind === 'hold') {
4278
+ // Recorded, not enforced here: this run has already done its work.
4279
+ // The hold is what the NEXT tick and the next Start will meet.
4280
+ await safety.raise({
4281
+ goalId: binding.goal.id,
4282
+ goalVersion: binding.goal.version,
4283
+ kind: verdict.hold,
4284
+ because: verdict.because,
4285
+ evidence: verdict.evidence,
4286
+ });
4287
+ log(`safety : ${verdict.because}`);
4288
+ }
4289
+ }
4290
+ // AFTER the observation, so this round is measured against what had
4291
+ // already passed BEFORE it. Updating first would fold this round's own
4292
+ // gates into its own baseline and make every round look like no progress.
4293
+ for (const step of rec.passingGates ?? [])
4294
+ gatesEverPassed.add(step);
4295
+ }
4296
+ }
4297
+ /**
4298
+ * And what a person could read about it later.
4299
+ *
4300
+ * The safety brake above stops the machine; this is what stops the NEXT
4301
+ * plan proposing the same thing. They are deliberately separate: safety is
4302
+ * authority over whether to spend, memory is only evidence — and P11.22 is
4303
+ * explicit that memory must never become loop-safety truth.
4304
+ */
4305
+ if (summary.lastFailure !== undefined && binding.kind === 'bound') {
4306
+ await rememberFailedAttempt(memoryStore, memoryKey, {
4307
+ runId,
4308
+ goalId: binding.goal.id,
4309
+ // The pursuit, not the item: an approach outlives the item that carried
4310
+ // it, and the next plan is written against the goal.
4311
+ subject: binding.goal.text,
4312
+ failureStep: summary.lastFailure.step,
4313
+ });
4314
+ }
4315
+ /**
4316
+ * SETTLED FIRST, THROWN SECOND, and never the other way round.
4317
+ *
4318
+ * The run has met a consequential action it could not file a question
4319
+ * about. That is a failure and must be reported as one — but the ledger is
4320
+ * the only account of what happened, and throwing before `run.ended` was
4321
+ * written would leave a run that looks live for ever to every reader on
4322
+ * both sides. So the ending is durable by the time this line runs, and it
4323
+ * says `aborted`: a failure this process could not continue past, which is
4324
+ * precisely what it is. What it must never say is that somebody is being
4325
+ * asked, because nobody is.
4326
+ *
4327
+ * A PLAIN ERROR, deliberately not a refusal. The likeliest cause is that
4328
+ * the server has not yet been told about this run, or could not be reached
4329
+ * at all — both of which the next attempt may well get past. A terminal
4330
+ * refusal would spend the pursuit permanently on a problem that fixes
4331
+ * itself.
4332
+ */
4333
+ if (approvalFailure !== null) {
4334
+ await ledgerForRun.append({
4335
+ ts: new Date().toISOString(),
4336
+ runId,
4337
+ projectId,
4338
+ lane: 'feature',
4339
+ kind: 'alert',
4340
+ actor: 'system',
4341
+ // Critical for the same reason a dead prover is: nothing else reaches a
4342
+ // phone from a detached run, and a run that keeps meeting an action it
4343
+ // cannot ask about will do it again tonight and every night after.
4344
+ data: { code: 'approval-unfiled', severity: 'critical', text: approvalFailure },
4345
+ });
4346
+ throw new Error(approvalFailure);
4347
+ }
4348
+ return summary;
4349
+ }
4350
+ finally {
4351
+ // Before the lease is released, so nothing can start a new run while an
4352
+ // agent from this one is still writing to the tree.
4353
+ const killed = children.killAll();
4354
+ if (killed > 0)
4355
+ log(`run : stopped ${killed} agent process(es) this run had started.`);
4356
+ // Released whatever happened, including on the way out of a throw: a lease
4357
+ // left behind is recoverable (the next process finds the pid dead) but it
4358
+ // makes an ordinary failure look like a crash.
4359
+ await releaseHold();
4360
+ signals.uninstall();
4361
+ // Stopping the sync flushes what the last round produced. Without this the
4362
+ // final rounds of a run — including run.ended — could be lost on exit, and
4363
+ // the phone would show a run that appears to still be going forever.
4364
+ if (sync) {
4365
+ await sync.stop();
4366
+ // A stream that died an hour ago said so an hour ago, and that line has
4367
+ // scrolled away. The end of the run is where someone looks to find out
4368
+ // whether their phone saw any of it.
4369
+ const { buffered, dropped, unacked } = sync.daemon.status();
4370
+ if (!sync.alive()) {
4371
+ log(`sync : ${buffered} events never reached your phone. \`looply report\` still has the run.`);
4372
+ }
4373
+ // The cap sheds the *oldest* events, so an overflowed mirror reads as a
4374
+ // run that started late rather than one that is missing anything — the
4375
+ // only place that reads honestly is a line saying how many went.
4376
+ if (dropped > 0) {
4377
+ log(`sync : ${dropped} events were dropped while the server was away — your phone's copy has a gap.`);
4378
+ }
4379
+ // A different failure from the two above, and the quietest one: the
4380
+ // server answered every request successfully and kept fewer events than
4381
+ // it was handed. Nothing about the run looks wrong from here, so if this
4382
+ // is not said, the only symptom is a phone showing a project at rest
4383
+ // while a run is plainly happening in this terminal.
4384
+ if (unacked > 0) {
4385
+ log(`sync : ${unacked} events reached the server but were not stored. Your phone's copy is incomplete;`);
4386
+ log(' `looply report` still has the whole run.');
4387
+ }
4388
+ }
4389
+ /**
4390
+ * THE LAST THING THIS RUN DOES, and it has to be last.
4391
+ *
4392
+ * The creation marker names THIS PROCESS, and it is the checkout's only
4393
+ * cover once `releaseHold` has given back the lease and the registry
4394
+ * record — which happens at the TOP of this block, while there is still
4395
+ * work below it. Clearing the marker any earlier reopened a window where a
4396
+ * concurrent `--reap` saw no lease, no record and no marker.
4397
+ *
4398
+ * ⚠️ BUT IT MUST BE CLEARED. Left behind, it is a permanent pid-shaped
4399
+ * claim on a finished checkout: once that pid is recycled the workspace
4400
+ * reads as busy for ever, invisible to `looply runs --reap` with nothing a
4401
+ * person could point at. The lease has the same weakness and is cleared for
4402
+ * the same reason.
4403
+ */
4404
+ /**
4405
+ * ⚠️ SURFACES GO BACK BEFORE THE START MARKER, and before anything else in
4406
+ * this teardown can throw. A surface held by a finished run blocks every
4407
+ * later run that declares it until the TTL lapses — six hours of a project
4408
+ * refusing to schedule itself, for a directory nobody is in.
4409
+ *
4410
+ * `releaseSurface` marks rather than deletes, so the ordinal still stands
4411
+ * and no later holder can be handed the same fence.
4412
+ */
4413
+ for (const fence of heldSurfaces)
4414
+ (0, surface_lease_1.releaseSurface)(cwd, fence, runId);
4415
+ (0, workspace_1.clearCreationMarker)(stateDir);
4416
+ }
4417
+ }
4418
+ //# sourceMappingURL=run-cmd.js.map