looply-cli 0.0.0-stage → 0.3.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 (793) 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 +2727 -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/serve-cmd.d.ts +115 -0
  605. package/dist/serve-cmd.d.ts.map +1 -0
  606. package/dist/serve-cmd.js +505 -0
  607. package/dist/serve-cmd.js.map +1 -0
  608. package/dist/serve.d.ts +167 -0
  609. package/dist/serve.d.ts.map +1 -0
  610. package/dist/serve.js +336 -0
  611. package/dist/serve.js.map +1 -0
  612. package/dist/server-client.d.ts +766 -0
  613. package/dist/server-client.d.ts.map +1 -0
  614. package/dist/server-client.js +475 -0
  615. package/dist/server-client.js.map +1 -0
  616. package/dist/session-adapter-runtime.d.ts +195 -0
  617. package/dist/session-adapter-runtime.d.ts.map +1 -0
  618. package/dist/session-adapter-runtime.js +373 -0
  619. package/dist/session-adapter-runtime.js.map +1 -0
  620. package/dist/session-checkpoint-runtime.d.ts +62 -0
  621. package/dist/session-checkpoint-runtime.d.ts.map +1 -0
  622. package/dist/session-checkpoint-runtime.js +268 -0
  623. package/dist/session-checkpoint-runtime.js.map +1 -0
  624. package/dist/session-control-transport.d.ts +106 -0
  625. package/dist/session-control-transport.d.ts.map +1 -0
  626. package/dist/session-control-transport.js +232 -0
  627. package/dist/session-control-transport.js.map +1 -0
  628. package/dist/session-observation-store.d.ts +98 -0
  629. package/dist/session-observation-store.d.ts.map +1 -0
  630. package/dist/session-observation-store.js +155 -0
  631. package/dist/session-observation-store.js.map +1 -0
  632. package/dist/session-observe-runtime.d.ts +123 -0
  633. package/dist/session-observe-runtime.d.ts.map +1 -0
  634. package/dist/session-observe-runtime.js +317 -0
  635. package/dist/session-observe-runtime.js.map +1 -0
  636. package/dist/session-store.d.ts +98 -0
  637. package/dist/session-store.d.ts.map +1 -0
  638. package/dist/session-store.js +116 -0
  639. package/dist/session-store.js.map +1 -0
  640. package/dist/sessions-cmd.d.ts +202 -0
  641. package/dist/sessions-cmd.d.ts.map +1 -0
  642. package/dist/sessions-cmd.js +609 -0
  643. package/dist/sessions-cmd.js.map +1 -0
  644. package/dist/settings-overlay.d.ts +53 -0
  645. package/dist/settings-overlay.d.ts.map +1 -0
  646. package/dist/settings-overlay.js +175 -0
  647. package/dist/settings-overlay.js.map +1 -0
  648. package/dist/setup-prompt.d.ts +34 -0
  649. package/dist/setup-prompt.d.ts.map +1 -0
  650. package/dist/setup-prompt.js +92 -0
  651. package/dist/setup-prompt.js.map +1 -0
  652. package/dist/setup.d.ts +103 -0
  653. package/dist/setup.d.ts.map +1 -0
  654. package/dist/setup.js +283 -0
  655. package/dist/setup.js.map +1 -0
  656. package/dist/shared-grants.d.ts +164 -0
  657. package/dist/shared-grants.d.ts.map +1 -0
  658. package/dist/shared-grants.js +242 -0
  659. package/dist/shared-grants.js.map +1 -0
  660. package/dist/signals.d.ts +55 -0
  661. package/dist/signals.d.ts.map +1 -0
  662. package/dist/signals.js +85 -0
  663. package/dist/signals.js.map +1 -0
  664. package/dist/source-cmd.d.ts +201 -0
  665. package/dist/source-cmd.d.ts.map +1 -0
  666. package/dist/source-cmd.js +684 -0
  667. package/dist/source-cmd.js.map +1 -0
  668. package/dist/source-verdict.d.ts +99 -0
  669. package/dist/source-verdict.d.ts.map +1 -0
  670. package/dist/source-verdict.js +182 -0
  671. package/dist/source-verdict.js.map +1 -0
  672. package/dist/spend-ledger.d.ts +350 -0
  673. package/dist/spend-ledger.d.ts.map +1 -0
  674. package/dist/spend-ledger.js +546 -0
  675. package/dist/spend-ledger.js.map +1 -0
  676. package/dist/stale-run-sweep.d.ts +79 -0
  677. package/dist/stale-run-sweep.d.ts.map +1 -0
  678. package/dist/stale-run-sweep.js +116 -0
  679. package/dist/stale-run-sweep.js.map +1 -0
  680. package/dist/start-attempts.d.ts +385 -0
  681. package/dist/start-attempts.d.ts.map +1 -0
  682. package/dist/start-attempts.js +613 -0
  683. package/dist/start-attempts.js.map +1 -0
  684. package/dist/status-cmd.d.ts +28 -0
  685. package/dist/status-cmd.d.ts.map +1 -0
  686. package/dist/status-cmd.js +116 -0
  687. package/dist/status-cmd.js.map +1 -0
  688. package/dist/stop-follow-up.d.ts +107 -0
  689. package/dist/stop-follow-up.d.ts.map +1 -0
  690. package/dist/stop-follow-up.js +172 -0
  691. package/dist/stop-follow-up.js.map +1 -0
  692. package/dist/surface-lease.d.ts +118 -0
  693. package/dist/surface-lease.d.ts.map +1 -0
  694. package/dist/surface-lease.js +485 -0
  695. package/dist/surface-lease.js.map +1 -0
  696. package/dist/sync.d.ts +54 -0
  697. package/dist/sync.d.ts.map +1 -0
  698. package/dist/sync.js +93 -0
  699. package/dist/sync.js.map +1 -0
  700. package/dist/syncing-ledger.d.ts +45 -0
  701. package/dist/syncing-ledger.d.ts.map +1 -0
  702. package/dist/syncing-ledger.js +53 -0
  703. package/dist/syncing-ledger.js.map +1 -0
  704. package/dist/temp-fixtures.d.ts +68 -0
  705. package/dist/temp-fixtures.d.ts.map +1 -0
  706. package/dist/temp-fixtures.js +38 -0
  707. package/dist/temp-fixtures.js.map +1 -0
  708. package/dist/token-window.d.ts +84 -0
  709. package/dist/token-window.d.ts.map +1 -0
  710. package/dist/token-window.js +143 -0
  711. package/dist/token-window.js.map +1 -0
  712. package/dist/tool-bridge.d.ts +177 -0
  713. package/dist/tool-bridge.d.ts.map +1 -0
  714. package/dist/tool-bridge.js +329 -0
  715. package/dist/tool-bridge.js.map +1 -0
  716. package/dist/tool-call-record.d.ts +197 -0
  717. package/dist/tool-call-record.d.ts.map +1 -0
  718. package/dist/tool-call-record.js +276 -0
  719. package/dist/tool-call-record.js.map +1 -0
  720. package/dist/tool-protocol.d.ts +135 -0
  721. package/dist/tool-protocol.d.ts.map +1 -0
  722. package/dist/tool-protocol.js +104 -0
  723. package/dist/tool-protocol.js.map +1 -0
  724. package/dist/tool-worker.d.ts +3 -0
  725. package/dist/tool-worker.d.ts.map +1 -0
  726. package/dist/tool-worker.js +478 -0
  727. package/dist/tool-worker.js.map +1 -0
  728. package/dist/toolchain-mirror.d.ts +86 -0
  729. package/dist/toolchain-mirror.d.ts.map +1 -0
  730. package/dist/toolchain-mirror.js +307 -0
  731. package/dist/toolchain-mirror.js.map +1 -0
  732. package/dist/trusted-git.d.ts +243 -0
  733. package/dist/trusted-git.d.ts.map +1 -0
  734. package/dist/trusted-git.js +920 -0
  735. package/dist/trusted-git.js.map +1 -0
  736. package/dist/ui.d.ts +76 -0
  737. package/dist/ui.d.ts.map +1 -0
  738. package/dist/ui.js +118 -0
  739. package/dist/ui.js.map +1 -0
  740. package/dist/untrusted-exec.d.ts +275 -0
  741. package/dist/untrusted-exec.d.ts.map +1 -0
  742. package/dist/untrusted-exec.js +479 -0
  743. package/dist/untrusted-exec.js.map +1 -0
  744. package/dist/untrusted-gate.d.ts +73 -0
  745. package/dist/untrusted-gate.d.ts.map +1 -0
  746. package/dist/untrusted-gate.js +226 -0
  747. package/dist/untrusted-gate.js.map +1 -0
  748. package/dist/uploader.d.ts +153 -0
  749. package/dist/uploader.d.ts.map +1 -0
  750. package/dist/uploader.js +204 -0
  751. package/dist/uploader.js.map +1 -0
  752. package/dist/verifier.d.ts +66 -0
  753. package/dist/verifier.d.ts.map +1 -0
  754. package/dist/verifier.js +170 -0
  755. package/dist/verifier.js.map +1 -0
  756. package/dist/verify-apply.d.ts +26 -0
  757. package/dist/verify-apply.d.ts.map +1 -0
  758. package/dist/verify-apply.js +36 -0
  759. package/dist/verify-apply.js.map +1 -0
  760. package/dist/verify-cmd.d.ts +44 -0
  761. package/dist/verify-cmd.d.ts.map +1 -0
  762. package/dist/verify-cmd.js +125 -0
  763. package/dist/verify-cmd.js.map +1 -0
  764. package/dist/verify-pass.d.ts +73 -0
  765. package/dist/verify-pass.d.ts.map +1 -0
  766. package/dist/verify-pass.js +103 -0
  767. package/dist/verify-pass.js.map +1 -0
  768. package/dist/welcome.d.ts +43 -0
  769. package/dist/welcome.d.ts.map +1 -0
  770. package/dist/welcome.js +116 -0
  771. package/dist/welcome.js.map +1 -0
  772. package/dist/workspace-provisioner.d.ts +65 -0
  773. package/dist/workspace-provisioner.d.ts.map +1 -0
  774. package/dist/workspace-provisioner.js +66 -0
  775. package/dist/workspace-provisioner.js.map +1 -0
  776. package/dist/workspace-quarantine.d.ts +64 -0
  777. package/dist/workspace-quarantine.d.ts.map +1 -0
  778. package/dist/workspace-quarantine.js +156 -0
  779. package/dist/workspace-quarantine.js.map +1 -0
  780. package/dist/workspace-residue-sweep.d.ts +113 -0
  781. package/dist/workspace-residue-sweep.d.ts.map +1 -0
  782. package/dist/workspace-residue-sweep.js +158 -0
  783. package/dist/workspace-residue-sweep.js.map +1 -0
  784. package/dist/workspace.d.ts +639 -0
  785. package/dist/workspace.d.ts.map +1 -0
  786. package/dist/workspace.js +2026 -0
  787. package/dist/workspace.js.map +1 -0
  788. package/package.json +52 -4
  789. package/win/CredProbe.cs +64 -0
  790. package/win/FakeClaude.cs +56 -0
  791. package/win/HandleProbe.cs +116 -0
  792. package/win/TokenProbe.cs +53 -0
  793. package/win/appcontainer.ps1 +500 -0
package/dist/bin.js ADDED
@@ -0,0 +1,2727 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.main = main;
5
+ const source_cmd_1 = require("./source-cmd");
6
+ const fs_1 = require("fs");
7
+ const tool_bridge_1 = require("./tool-bridge");
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_child_process_1 = require("node:child_process");
10
+ const path_1 = require("path");
11
+ const config_1 = require("./config");
12
+ const self_repo_id_1 = require("./self-repo-id");
13
+ const programme_mode_1 = require("./programme-mode");
14
+ const trusted_git_1 = require("./trusted-git");
15
+ const doctor_1 = require("./doctor");
16
+ const integration_runtime_1 = require("./integration-runtime");
17
+ const publication_recorder_1 = require("./publication-recorder");
18
+ const observation_inventory_1 = require("./observation-inventory");
19
+ const evidence_facts_1 = require("./evidence-facts");
20
+ const integration_deps_1 = require("./integration-deps");
21
+ const lane_claims_1 = require("./lane-claims");
22
+ const integration_runtime_2 = require("./integration-runtime");
23
+ const agent_contained_1 = require("./agent-contained");
24
+ const exec_domains_1 = require("./exec-domains");
25
+ const model_transport_1 = require("./model-transport");
26
+ const provider_state_1 = require("./provider-state");
27
+ const providers_1 = require("./providers");
28
+ const provider_preflight_1 = require("./provider-preflight");
29
+ const init_detect_1 = require("./init-detect");
30
+ const report_cmd_1 = require("./report-cmd");
31
+ const tool_call_record_1 = require("./tool-call-record");
32
+ const token_window_1 = require("./token-window");
33
+ const status_cmd_1 = require("./status-cmd");
34
+ const goal_view_1 = require("./goal-view");
35
+ const run_cmd_1 = require("./run-cmd");
36
+ const start_attempts_1 = require("./start-attempts");
37
+ const node_os_1 = require("node:os");
38
+ const autonomy_authority_1 = require("./autonomy-authority");
39
+ const scan_cmd_1 = require("./scan-cmd");
40
+ const verify_cmd_1 = require("./verify-cmd");
41
+ const queue_write_lease_1 = require("./queue-write-lease");
42
+ const workspace_quarantine_1 = require("./workspace-quarantine");
43
+ const queue_view_1 = require("./queue-view");
44
+ const queue_store_1 = require("./queue-store");
45
+ const ledger_store_1 = require("./ledger-store");
46
+ const git_driver_1 = require("./git-driver");
47
+ const repos_cmd_1 = require("./repos-cmd");
48
+ const serve_cmd_1 = require("./serve-cmd");
49
+ const core_1 = require("@looply/core");
50
+ const sessions_cmd_1 = require("./sessions-cmd");
51
+ const session_observe_runtime_1 = require("./session-observe-runtime");
52
+ const evidence_cmd_1 = require("./evidence-cmd");
53
+ const retention_apply_runtime_1 = require("./retention-apply-runtime");
54
+ const session_store_1 = require("./session-store");
55
+ const compatible_sets_1 = require("./compatible-sets");
56
+ const workspace_1 = require("./workspace");
57
+ const run_refusal_marks_1 = require("./run-refusal-marks");
58
+ const daemon_cmd_1 = require("./daemon-cmd");
59
+ const machine_store_1 = require("./machine-store");
60
+ const server_client_1 = require("./server-client");
61
+ const session_adapter_runtime_1 = require("./session-adapter-runtime");
62
+ const session_control_transport_1 = require("./session-control-transport");
63
+ const session_checkpoint_runtime_1 = require("./session-checkpoint-runtime");
64
+ const session_observation_store_1 = require("./session-observation-store");
65
+ const welcome_1 = require("./welcome");
66
+ const agent_cmd_1 = require("./agent-cmd");
67
+ const ui_1 = require("./ui");
68
+ const setup_1 = require("./setup");
69
+ const setup_prompt_1 = require("./setup-prompt");
70
+ const integration_1 = require("./integration");
71
+ /**
72
+ * `looply` entry point.
73
+ *
74
+ * Only the commands that are implemented are listed. A command that prints
75
+ * "not implemented yet" is worse than one that does not exist, because it looks
76
+ * like a broken feature rather than an unbuilt one.
77
+ */
78
+ const USAGE = `looply — an autonomous build loop you drive from your phone
79
+
80
+ usage:
81
+ looply pair [--server <url>] pair this machine with your phone
82
+ looply daemon [--once] [--yes] stay connected: presence, and ship the ledger
83
+ looply serve add [path] serve this repository (or <path>) from one machine-wide supervisor
84
+ looply serve remove <path> stop serving a repository
85
+ looply serve list the served repositories, and whether each still has .looply/config.yaml
86
+ looply serve run the supervisor: one daemon per served repository, restarted if it stops
87
+ looply serve install | uninstall start the supervisor at login (Windows); prints a service template elsewhere
88
+ looply init profile the repo and draft .looply/config.yaml
89
+ looply doctor [--config <path>] [--provider]
90
+ validate the config and run one full gate pass; --provider also asks the model to answer once
91
+ looply scan [--lens a,b] [--dry-run] read the code through several lenses and fill the queue
92
+ looply verify [--max N] check unverified items against the code, so a run can pick them
93
+ looply queue [--all] everything waiting, and why
94
+ looply integrate --init <ref> create looply/integration from a base you name
95
+ looply integrate --branch <name> [--goal <id>]
96
+ offer one finished branch to the integration queue:
97
+ fast-forward or nothing, checked at the commit that
98
+ would actually be published (needs run.integration)
99
+ looply runs [--reap] what this project is running; --reap retires proven-dead leftovers
100
+ looply runs --release <runId|slot-N|claim-lock>
101
+ clear a leftover a recycled pid makes look alive
102
+ looply source add <file> [--name <n>]
103
+ hand a Markdown (.md) or plain-text (.txt) document to
104
+ this project, verbatim, from the machine that holds it
105
+ looply source list what this project has been given
106
+ looply source show <id> one source: its statements, what each IS, where each came from,
107
+ and where each requirement stands against this repository
108
+ looply source reconcile <id> measure where each requirement stands against this repository
109
+ (HEAD's tree, by name — a place to look, never "implemented"), and record it
110
+ looply source coverage <id> which requirements no goal serves, which are served, and which a person
111
+ evidenced — a finished queue is not coverage
112
+ looply stop finish the round in flight, then stop
113
+ looply pause | looply resume hold between rounds, and let it go again
114
+ looply run [--hours N] [--dry-run] [--in <workspace> | --new-workspace]
115
+ start a run: pick, implement, gate, prove, keep or discard
116
+ --in runs it in another checkout of this repository, so two
117
+ runs can work at once without sharing a tree
118
+ --try-once-more spends a stopped goal's ONE retry (never
119
+ turns the brake off; the next failure stops it again)
120
+ looply status one-screen snapshot from .looply/state/status.json
121
+ looply report run report from the local ledger, alerts first
122
+ looply goal <id> one goal from this machine's records: the grant and the lane's answer,
123
+ compiles, proposals, the lane's attempts, each run's rounds — read-only
124
+ looply goals every mirrored goal, one line each, with its grant's standing
125
+ looply repos [--accept] [--published <repo>]
126
+ the other repositories this programme spans: where each one is,
127
+ and whether the revisions somebody accepted together still hold
128
+ looply evidence what this project has accumulated, and what the retention
129
+ policy would do with it. It deletes nothing.
130
+ looply sessions [--offer <file>] [--ack <id>]
131
+ sessions this machine did not start, asking to be managed;
132
+ observe-only — authorizing control is the server's to give
133
+ looply version
134
+
135
+ not built yet: item
136
+ `;
137
+ function loadConfig(path) {
138
+ const full = (0, path_1.resolve)(path);
139
+ let text;
140
+ try {
141
+ text = (0, fs_1.readFileSync)(full, 'utf8');
142
+ }
143
+ catch {
144
+ throw new config_1.ConfigError('<file>', `cannot read ${full}`);
145
+ }
146
+ return (0, config_1.parseConfig)(text);
147
+ }
148
+ /* istanbul ignore next — real fs and env; the logic lives in doctor.ts */
149
+ function nodeDoctorDeps() {
150
+ // Lifted to integration-deps.ts (M5-1) so doctor, the integrate command and the daemon ask the same questions.
151
+ return (0, integration_deps_1.nodeDoctorDepsFor)(process.cwd());
152
+ }
153
+ /* istanbul ignore next — real fs, tty and cwd; the logic lives in setup.ts */
154
+ async function init(argv) {
155
+ const target = (0, path_1.resolve)('.looply/config.yaml');
156
+ // Never overwrite: by the time init is re-run the file is the user's, full of
157
+ // hand-tuned rails and gates a fresh draft would silently throw away.
158
+ if ((0, fs_1.existsSync)(target)) {
159
+ process.stderr.write(`init: ${target} already exists — move it aside first for a fresh draft\n`);
160
+ return 1;
161
+ }
162
+ const profile = (0, init_detect_1.detectRepo)({
163
+ readFile: (p) => {
164
+ try {
165
+ return (0, fs_1.readFileSync)((0, path_1.resolve)(p), 'utf8');
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ },
171
+ }, (0, path_1.basename)(process.cwd()));
172
+ // What detection found, before anything is asked — so the questions arrive
173
+ // with context rather than out of nowhere.
174
+ process.stdout.write((0, ui_1.box)((0, ui_1.fields)([
175
+ ['project', profile.name],
176
+ ['manager', `${profile.packageManager} ${(0, ui_1.dim)(`(${profile.packageManagerEvidence})`)}`],
177
+ ...(profile.workspaces.length
178
+ ? [['spaces', profile.workspaces.join(', ')]]
179
+ : []),
180
+ [
181
+ 'checks',
182
+ [profile.typecheck?.name, profile.testRunner?.name].filter(Boolean).join(', ') ||
183
+ (0, ui_1.dim)('none detected'),
184
+ ],
185
+ ]), { title: 'what this project looks like' }) + '\n');
186
+ // --yes for CI and for anyone who would rather edit the file than answer
187
+ // questions. Non-interactive stdin takes the same path: prompting a pipe
188
+ // hangs forever, which is the worst possible failure for a setup command.
189
+ const useDefaults = argv.includes('--yes') || argv.includes('-y') || !process.stdin.isTTY;
190
+ const asker = (0, setup_prompt_1.nodeAsker)();
191
+ let answers;
192
+ try {
193
+ answers = await (0, setup_prompt_1.runSetup)({
194
+ profile,
195
+ asker,
196
+ write: (s) => process.stdout.write(s),
197
+ useDefaults,
198
+ });
199
+ }
200
+ finally {
201
+ asker.close();
202
+ }
203
+ (0, fs_1.mkdirSync)((0, path_1.resolve)('.looply'), { recursive: true });
204
+ (0, fs_1.writeFileSync)(target, (0, setup_1.renderConfig)(profile, answers));
205
+ const paired = (() => {
206
+ try {
207
+ return new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load() !== null;
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ })();
213
+ process.stdout.write((0, setup_prompt_1.renderSetupDone)((0, setup_1.summarise)(answers, profile), target, paired));
214
+ if (useDefaults && !answers.rails.length) {
215
+ process.stdout.write(` ${(0, ui_1.warn)('no rails were set — nothing in this repo is off limits. Edit forbidden_paths before a long run.')}\n\n`);
216
+ }
217
+ return 0;
218
+ }
219
+ async function doctor(argv) {
220
+ const at = argv.indexOf('--config');
221
+ const path = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
222
+ if (!path) {
223
+ process.stderr.write('doctor: --config needs a path\n');
224
+ return 1;
225
+ }
226
+ const cfg = loadConfig(path);
227
+ process.stdout.write(`project : ${cfg.project.name}\n`);
228
+ process.stdout.write(`budget : ${cfg.run.minHours}h, up to ${cfg.run.maxRounds} rounds\n`);
229
+ process.stdout.write(`proof : ${cfg.proof.enabled ? 'on' : 'OFF'}\n`);
230
+ process.stdout.write(`notify : ${cfg.notify.channel} every ${cfg.notify.everyMin}min\n`);
231
+ process.stdout.write('gates :\n');
232
+ for (const g of cfg.gates) {
233
+ const bits = [g.required ? 'required' : null, g.when ? `when ${g.when}` : null].filter(Boolean);
234
+ process.stdout.write(` - ${g.name}${bits.length ? ` (${bits.join(', ')})` : ''}: ${g.run}\n`);
235
+ }
236
+ process.stdout.write(`rails : ${cfg.rails.forbiddenPaths.join(', ') || '(none)'}\n`);
237
+ process.stdout.write(`mode : ${(0, programme_mode_1.describeProgramme)(cfg)}\n`);
238
+ /**
239
+ * WHAT THE LAST WINDOW COST IN TOKENS, WHICH NOTHING HAS EVER SHOWN.
240
+ *
241
+ * These figures have been on every spend row since they were first parsed,
242
+ * and the only reader of that ledger sums dollars — the capability audit's
243
+ * E-05. This is the one command that already reports what this machine is and
244
+ * what it is about to do, so it is where they go. The filesystem read is here
245
+ * because the filesystem is; what may be said, and what may not, is decided
246
+ * in `token-window.ts`.
247
+ *
248
+ * The machine's own money window when it declares one, so the two figures a
249
+ * person compares are over the same hours; otherwise the stated default.
250
+ */
251
+ const tokenWindowMs = cfg.machine?.envelope?.windowMs ?? token_window_1.DEFAULT_TOKEN_WINDOW_MS;
252
+ process.stdout.write((0, token_window_1.tokenLine)((0, token_window_1.tokensInWindow)(Date.now(), tokenWindowMs), tokenWindowMs) + String.fromCharCode(10));
253
+ const warnings = (0, config_1.configWarnings)(cfg);
254
+ for (const w of warnings)
255
+ process.stdout.write(`warning : ${w}\n`);
256
+ // The integration ref, asked here rather than at round 1 of a run (M6-12).
257
+ const refWarning = await (0, doctor_1.integrationRefWarning)(cfg, new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(process.cwd())));
258
+ if (refWarning !== null)
259
+ process.stdout.write(`warning : ${refWarning}\n`);
260
+ process.stdout.write('doctor : running interlocks and one full gate pass (no agent call)\n');
261
+ const report = await (0, doctor_1.runDoctorGate)(cfg, nodeDoctorDeps());
262
+ /**
263
+ * 🔴 THIS SAID "UNTRUSTED EXECUTION IS CONTAINED" AND MEANT ONLY THE GATE.
264
+ *
265
+ * MEASURED on a machine printing exactly that line: a provider executable
266
+ * appended to and TRUNCATED the machine spend ledger, and wrote a path
267
+ * outside every declared `agent.runtime` grant. Both are true at once,
268
+ * because the AppContainer is applied to GATE STEPS — the project's own
269
+ * commands — and the contained-provider path was deliberately removed from
270
+ * the transport after two phases established the provider cannot run inside
271
+ * one.
272
+ *
273
+ * A single PASS line covering all subprocesses is therefore a claim nobody
274
+ * can honour. Four separate facts, four separate lines, and the one that is
275
+ * an ASSUMPTION says so in the word `assumed`.
276
+ */
277
+ if (report.containment) {
278
+ const c = report.containment;
279
+ process.stdout.write(c.available
280
+ ? `doctor : gate steps are contained (${c.backend})` + String.fromCharCode(10)
281
+ : `doctor : gate steps are NOT contained (${c.backend}) — ${c.because ?? 'unavailable'}` +
282
+ String.fromCharCode(10));
283
+ }
284
+ // The provider is not confined by anything, on any machine, in this build.
285
+ // Stated unconditionally because it does not vary: see `model-transport`.
286
+ process.stdout.write('doctor : the provider process is NOT confined — it runs as you, and is TRUSTED by' +
287
+ String.fromCharCode(10) +
288
+ ' assumption. Run only provider binaries you trust with this machine.' +
289
+ String.fromCharCode(10));
290
+ process.stdout.write(`doctor : model tool access is mediated (${tool_bridge_1.TOOL_POLICIES.implementer.tools.join(', ')} for the` +
291
+ String.fromCharCode(10) +
292
+ ` implementer; ${tool_bridge_1.TOOL_POLICIES.verifier.tools.join(', ')} for the verifier), and` +
293
+ String.fromCharCode(10) +
294
+ ' off-project paths are refused at the bridge.' +
295
+ String.fromCharCode(10));
296
+ if (report.abort) {
297
+ process.stderr.write(`ABORT : ${report.abort.reason} (code ${report.abort.code})\n`);
298
+ return 1;
299
+ }
300
+ for (const s of report.steps) {
301
+ const label = s.ok ? 'ok ' : 'FAIL';
302
+ const why = s.timedOut ? ' — timed out' : '';
303
+ process.stdout.write(` ${label} ${s.step} (${s.ms}ms)${why}\n`);
304
+ if (!s.ok && s.output)
305
+ process.stdout.write(`${s.output.replace(/\s+$/, '')}\n`);
306
+ }
307
+ process.stdout.write(report.ok ? 'doctor : all gate steps pass\n' : 'doctor : gate steps failed\n');
308
+ /**
309
+ * 🔴 THE AGENT CHECK IS OPT-IN, AND ITS ABSENCE WAS A REAL HOLE.
310
+ *
311
+ * `doctor` deliberately makes NO agent call — it exists to find a wrong `tsc`
312
+ * in ninety seconds, and that was the right decision. But it left the other
313
+ * half unchecked: a wrong AGENT command was found at round 1 of an overnight
314
+ * run, by which point a person is asleep.
315
+ *
316
+ * ⚠️ OPT-IN BECAUSE IT SPENDS A MODEL CALL. Making it part of every `doctor`
317
+ * would put a cost on the check people run most often, and they would stop
318
+ * running it.
319
+ */
320
+ if (argv.includes('--provider')) {
321
+ const resolved = (0, providers_1.resolveProvider)({
322
+ ...(cfg.provider === undefined ? {} : { provider: cfg.provider }),
323
+ spec: cfg.agent,
324
+ });
325
+ if (!('spec' in resolved)) {
326
+ process.stderr.write(`provider: ${resolved.message}\n`);
327
+ return 1;
328
+ }
329
+ if (resolved.warning !== null)
330
+ process.stdout.write(`warning : ${resolved.warning}\n`);
331
+ /**
332
+ * 🔴 READINESS FIRST, AND IT COSTS NOTHING. `auth status` is local and prints
333
+ * the profile’s own state; asking the model whether it is logged in spends a
334
+ * turn to learn something a file already knows. More importantly it answers
335
+ * the RIGHT question: this is the profile and environment the run uses.
336
+ *
337
+ * ⚠️ AND IT REPORTS THE REASON IT WAS GIVEN, NOT A GUESS. Five states are kept
338
+ * apart in `provider-state.ts` precisely because only ONE of them means "log
339
+ * in"; flattening them sends somebody to re-authenticate an account that was
340
+ * never the trouble. Measured before this existed: an unauthenticated profile
341
+ * was reported as `refused`, advising a comparison of command-line flags.
342
+ */
343
+ const profile = (0, provider_state_1.providerProfileDir)();
344
+ const exe = resolved.spec.executable ?? (0, provider_state_1.providerExecutableFor)((0, agent_contained_1.claudePackageDir)());
345
+ if (exe === null) {
346
+ process.stdout.write('provider: no provider runtime was found — Claude Code is not installed beside node,\n');
347
+ process.stdout.write(' and agent.executable names none.\n');
348
+ return 1;
349
+ }
350
+ const sessionDir = (0, model_transport_1.providerSessionDir)(process.cwd(), 'implementer');
351
+ const ready = (0, provider_state_1.checkProviderAuth)(exe, { ...(0, exec_domains_1.buildProviderEnv)(sessionDir), CLAUDE_CONFIG_DIR: profile });
352
+ if (!ready.ok) {
353
+ process.stdout.write(`provider: not ready — ${ready.reason}\n`);
354
+ process.stdout.write(` ${ready.because}\n`);
355
+ process.stdout.write(` profile: ${ready.profile}\n`);
356
+ process.stdout.write(' no model call was spent finding this out.\n');
357
+ return 1;
358
+ }
359
+ process.stdout.write(`provider: logged in (${ready.method}) for ${ready.profile}\n`);
360
+ process.stdout.write('provider: asking it to answer once (this spends a model call)\n');
361
+ // `exe` above is the path `checkProviderAuth` just used successfully, and
362
+ // the one the run would spawn. Handing it over keeps the check from
363
+ // refusing a provider the run starts perfectly well.
364
+ const pre = await (0, provider_preflight_1.preflightProvider)(resolved.spec, (0, provider_preflight_1.nodePreflightDeps)(resolved.spec.runtime ?? [], profile, exe));
365
+ process.stdout.write(`provider: ${pre.verdict} (${pre.ms}ms)\n`);
366
+ if (pre.detail)
367
+ process.stdout.write(` ${pre.detail}\n`);
368
+ if (pre.advice)
369
+ process.stdout.write(` ${pre.advice}\n`);
370
+ if (!(0, provider_preflight_1.preflightPasses)(pre))
371
+ return 1;
372
+ }
373
+ return report.ok ? 0 : 1;
374
+ }
375
+ /* istanbul ignore next — real fs and clock; the logic lives in status-cmd.ts */
376
+ function status() {
377
+ let text;
378
+ try {
379
+ text = (0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8');
380
+ }
381
+ catch (e) {
382
+ // Only "not there yet" maps to the friendly message; a permission error
383
+ // reported as "no run yet" would hide a real problem behind a calm screen.
384
+ if (e.code !== 'ENOENT')
385
+ throw e;
386
+ text = null;
387
+ }
388
+ const { out, code } = (0, status_cmd_1.renderStatus)(text, Date.now());
389
+ (code === 0 ? process.stdout : process.stderr).write(out);
390
+ return code;
391
+ }
392
+ /* istanbul ignore next — real fs; the logic lives in report-cmd.ts */
393
+ function report() {
394
+ let text;
395
+ try {
396
+ text = (0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/events.jsonl'), 'utf8');
397
+ }
398
+ catch (e) {
399
+ // Same rule as status: only "not there yet" is friendly; a permission
400
+ // error reported as "no run yet" would hide a real problem.
401
+ if (e.code !== 'ENOENT')
402
+ throw e;
403
+ text = null;
404
+ }
405
+ // The record of what the bridge served, when there is one. Read here for the
406
+ // same reason the ledger is: the filesystem stays out of report-cmd.ts. Its
407
+ // absence is the ordinary case — a scan, a verify, or a run before this
408
+ // existed — and renders nothing at all.
409
+ let toolText;
410
+ try {
411
+ toolText = (0, fs_1.readFileSync)((0, path_1.resolve)(`.looply/state/${tool_call_record_1.TOOL_CALL_FILE}`), 'utf8');
412
+ }
413
+ catch (e) {
414
+ if (e.code !== 'ENOENT')
415
+ throw e;
416
+ toolText = null;
417
+ }
418
+ const { out, code } = (0, report_cmd_1.renderReport)(text, toolText);
419
+ (code === 0 ? process.stdout : process.stderr).write(out);
420
+ return code;
421
+ }
422
+ /* istanbul ignore next — real fs; the logic lives in goal-view.ts */
423
+ async function goal(argv) {
424
+ const id = argv[0];
425
+ if (id === undefined || id.startsWith('--')) {
426
+ process.stderr.write('goal: which one? usage: looply goal <id>\n');
427
+ return 64;
428
+ }
429
+ const read = await (0, goal_view_1.readGoalFacts)(process.cwd(), id, { now: () => Date.now() });
430
+ if (!read.ok) {
431
+ process.stderr.write(`goal : ${read.because}\n`);
432
+ return read.code;
433
+ }
434
+ process.stdout.write((0, goal_view_1.renderGoalView)(read.facts, Date.now()));
435
+ return 0;
436
+ }
437
+ /* istanbul ignore next — real fs; the logic lives in goal-view.ts */
438
+ /**
439
+ * `looply repos` deps, against this checkout.
440
+ *
441
+ * 🔴 THE REVISION IS THE INTEGRATION REF WHERE THERE IS ONE, and HEAD otherwise
442
+ * — and it SAYS which. A compatible set is about what would be published, and
443
+ * for a project whose runs converge on `looply/integration` that is the
444
+ * integration ref, not whatever branch somebody left themselves on.
445
+ *
446
+ * ⚠️ NULL ON ANY FAILURE, never a guess. An unreadable repository must reach
447
+ * `combinedAcceptance` as absent so it answers `unmeasurable`; a fabricated sha
448
+ * would turn "I could not look" into "it has drifted".
449
+ */
450
+ /**
451
+ * Adapters THIS BUILD speaks. A constant, not configuration: what a build can
452
+ * talk to is a property of the build, and letting a config name an adapter the
453
+ * code does not implement is how a session gets a label nothing backs.
454
+ */
455
+ /** The same digest `daemon-cmd.ts` and `run-cmd.ts` key a project on. */
456
+ const sha256 = (text) => (0, node_crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex');
457
+ /**
458
+ * ⚠️ HOW LONG ONE `--direct` WAITS, AND HOW OFTEN IT LOOKS.
459
+ *
460
+ * An unbounded wait on a directory a SESSION writes is this process hanging on
461
+ * the cooperation of the thing it is directing. Ten seconds is long enough for
462
+ * an adapter that is listening and short enough that one that is not does not
463
+ * hold a terminal.
464
+ */
465
+ const DIRECT_WAIT_MS = 10_000;
466
+ const DIRECT_POLL_MS = 250;
467
+ const SUPPORTED_SESSION_ADAPTERS = ['looply-session-v1'];
468
+ /**
469
+ * `looply sessions` deps, against this checkout.
470
+ *
471
+ * 🔴 EVERY MEASUREMENT IS TAKEN HERE, AND NOTHING IS TAKEN FROM THE CLAIM. The
472
+ * claim supplies only the QUESTION — which workspace, which pid, which sha —
473
+ * and each answer comes from the machine. A field that could not be established
474
+ * is `null`, which `sessionAdoptionVerdict` turns into `unmeasurable`.
475
+ */
476
+ /**
477
+ * `looply evidence` deps: the evidence stores of THIS project, on disk.
478
+ *
479
+ * 🔴 TWO STORES, NAMED, AND THE REST DELIBERATELY ABSENT. The policy's docblock
480
+ * names three places evidence accumulates. These are the two whose class is
481
+ * unambiguous from what they are:
482
+ *
483
+ * rejected/<runId>/ a candidate a round threw away — bounded raw diagnostics
484
+ * from a FAILED attempt, which is `failure-diagnostic`
485
+ * almost word for word.
486
+ * retired/<runId>/ the record a reaped run left behind. DURABLE: the reaper
487
+ * keeps these on purpose — they are "the evidence `--reap`
488
+ * needs" — so an age window must never touch them.
489
+ *
490
+ * ⚠️ The evidence CACHE and the compile transcript are NOT inventoried here.
491
+ * They are single files holding many entries, and the policy's unit is an
492
+ * artifact; splitting them needs a per-entry decision about what each one is,
493
+ * and guessing that in order to report a bigger number would be the opposite of
494
+ * the point. Recorded as unfinished rather than approximated.
495
+ *
496
+ * 🔴 `owned: true` IS ASSERTED ONLY FOR DIRECTORIES LOOPLY ITSELF WRITES.
497
+ * Both of these live under `.looply/state/` and are created by the round runner
498
+ * and the reaper. Nothing outside those paths is claimed, because the policy
499
+ * treats an unowned artifact as untouchable and the whole value of that rule is
500
+ * that it is not handed out loosely.
501
+ */
502
+ function nodeEvidenceDeps() {
503
+ const stateDir = (0, git_driver_1.projectStateDir)(process.cwd());
504
+ return {
505
+ artifacts: async () => {
506
+ const out = [];
507
+ const walk = (store, cls) => {
508
+ const root = (0, path_1.join)(stateDir, store);
509
+ if (!(0, fs_1.existsSync)(root))
510
+ return;
511
+ for (const entry of (0, fs_1.readdirSync)(root, { withFileTypes: true })) {
512
+ const path = (0, path_1.join)(root, entry.name);
513
+ let bytes = 0;
514
+ let createdAtMs = 0;
515
+ try {
516
+ // A directory's size is the sum of what it holds; a stat on the
517
+ // directory itself reports the entry, not the contents.
518
+ const stack = [path];
519
+ while (stack.length > 0) {
520
+ const at = stack.pop();
521
+ const st = (0, fs_1.statSync)(at);
522
+ createdAtMs = createdAtMs === 0 ? st.mtimeMs : Math.min(createdAtMs, st.mtimeMs);
523
+ if (st.isDirectory()) {
524
+ for (const child of (0, fs_1.readdirSync)(at))
525
+ stack.push((0, path_1.join)(at, child));
526
+ }
527
+ else {
528
+ bytes += st.size;
529
+ }
530
+ }
531
+ }
532
+ catch {
533
+ // Unreadable is not evidence about size. Left out rather than
534
+ // reported as zero, which would understate a store on purpose.
535
+ continue;
536
+ }
537
+ out.push({ id: entry.name, store, bytes, createdAtMs, class: cls, owned: true });
538
+ }
539
+ };
540
+ walk('rejected', 'failure-diagnostic');
541
+ walk('retired', 'durable');
542
+ /**
543
+ * 🔴 THE THIRD STORE, AND THE ONE THIS PRODUCT PROMISED ITSELF.
544
+ * `session-observation-store.ts` says observations get "a root a
545
+ * retention policy can be pointed at". Nothing pointed it until now, so
546
+ * every watch ever taken was still on disk and `looply evidence` could
547
+ * only list the directory under *uninventoried*. §5 D5: *"do not claim
548
+ * complete retention from an inventory of only two folders."*
549
+ *
550
+ * ⚠️ ITS CLASSIFICATION IS PER FILE AND COMES FROM THE RECORD, not from
551
+ * the directory it sits in — a session holds watches of different ages
552
+ * that ended in different ways. `inventoryObservations` reads each one;
553
+ * anything it cannot read is named under `uninventoried` instead of
554
+ * being planned over.
555
+ */
556
+ out.push(...(0, observation_inventory_1.inventoryObservations)(stateDir).artifacts);
557
+ return out;
558
+ },
559
+ /**
560
+ * Everything under the state dir that the two classified stores do not
561
+ * cover. Reported, never planned over — see `reportUninventoried`.
562
+ */
563
+ uninventoried: async () => {
564
+ const out = [];
565
+ if (!(0, fs_1.existsSync)(stateDir))
566
+ return out;
567
+ /** `null` when it could not be sized — never 0, which would be a claim. */
568
+ const sizeOf = (path) => {
569
+ let bytes = 0;
570
+ try {
571
+ const stack = [path];
572
+ while (stack.length > 0) {
573
+ const at = stack.pop();
574
+ const st = (0, fs_1.statSync)(at);
575
+ if (st.isDirectory())
576
+ for (const child of (0, fs_1.readdirSync)(at))
577
+ stack.push((0, path_1.join)(at, child));
578
+ else
579
+ bytes += st.size;
580
+ }
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ return bytes;
586
+ };
587
+ for (const entry of (0, fs_1.readdirSync)(stateDir, { withFileTypes: true })) {
588
+ /**
589
+ * 🔴 THE CLASSIFIED STORES ARE SKIPPED HERE — `artifacts()` counts them
590
+ * — BUT ONLY FOR WHAT IT CAN READ. Its walk drops an entry it cannot
591
+ * `stat`, and this scan used to skip the whole directory, so such an
592
+ * entry was counted by NEITHER and named by neither. It is picked up
593
+ * here instead, under its store, so nothing falls between the two.
594
+ *
595
+ * ⚠️ Asked independently rather than by sharing state with
596
+ * `artifacts()`, which would make this answer depend on the order the
597
+ * two happen to be called in.
598
+ */
599
+ /**
600
+ * 🔴 CLASSIFIED NOW, SO IT IS NOT COUNTED TWICE — but what could not
601
+ * be READ is still named, with the reason, because that is the whole
602
+ * job of this list. A record that does not parse is exactly the entry
603
+ * a person needs to see; it is the one the policy refused to touch.
604
+ */
605
+ if (entry.name === observation_inventory_1.OBSERVATIONS_STORE) {
606
+ for (const why of (0, observation_inventory_1.inventoryObservations)(stateDir).unreadable) {
607
+ out.push({ name: `${observation_inventory_1.OBSERVATIONS_STORE}/${why}`, bytes: null });
608
+ }
609
+ continue;
610
+ }
611
+ if (entry.name === 'rejected' || entry.name === 'retired') {
612
+ const root = (0, path_1.join)(stateDir, entry.name);
613
+ let children;
614
+ try {
615
+ children = (0, fs_1.readdirSync)(root);
616
+ }
617
+ catch {
618
+ out.push({ name: entry.name, bytes: null });
619
+ continue;
620
+ }
621
+ for (const child of children) {
622
+ if (sizeOf((0, path_1.join)(root, child)) === null) {
623
+ out.push({ name: `${entry.name}/${child}`, bytes: null });
624
+ }
625
+ }
626
+ continue;
627
+ }
628
+ out.push({ name: entry.name, bytes: sizeOf((0, path_1.join)(stateDir, entry.name)) });
629
+ }
630
+ return out;
631
+ },
632
+ policy: evidence_cmd_1.evidencePolicy,
633
+ nowMs: () => Date.now(),
634
+ out: (l) => process.stdout.write(l + '\n'),
635
+ err: (l) => process.stderr.write(l + '\n'),
636
+ /* ── what `--apply` needs, and nothing else uses ────────────────────── */
637
+ pathOf: (a) => (0, path_1.join)(stateDir, a.store, a.id),
638
+ stateDir: () => stateDir,
639
+ apply: (intent) => (0, retention_apply_runtime_1.applyRetention)(intent, {
640
+ nowMs: () => Date.now(),
641
+ policy: (0, evidence_cmd_1.evidencePolicy)(),
642
+ facts: (id) => (0, evidence_facts_1.artifactFacts)(stateDir, id),
643
+ journalPath: (0, path_1.join)(stateDir, 'retention-apply.jsonl'),
644
+ lockPath: (0, path_1.join)(stateDir, 'retention-apply.lock'),
645
+ }),
646
+ };
647
+ }
648
+ function nodeSessionsDeps() {
649
+ const cwd = process.cwd();
650
+ const cfg = loadConfig((0, path_1.join)(cwd, git_driver_1.STATE_DIR, 'config.yaml'));
651
+ const stateDir = (0, git_driver_1.projectStateDir)(cwd);
652
+ const store = new session_store_1.SessionStore(stateDir, ledger_store_1.nodeLedgerFs);
653
+ let opened = false;
654
+ const open = async () => {
655
+ if (!opened) {
656
+ await store.open();
657
+ opened = true;
658
+ }
659
+ };
660
+ /**
661
+ * 🔴 ONE TRANSPORT, TWO READERS. `serverControl` and `directionContext` are
662
+ * the same read narrowed two ways, and building them from one object is what
663
+ * keeps that true: two separately-constructed transports would be two
664
+ * separate reads of the grant list, and the verb a caller checks could be
665
+ * checked against a scope the other read never saw.
666
+ */
667
+ const transportDeps = {
668
+ machine: () => {
669
+ const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
670
+ if (identity === null)
671
+ return null;
672
+ const projectId = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).projectFor((0, machine_store_1.repoPathHash)(cwd, sha256));
673
+ if (projectId === null)
674
+ return null;
675
+ return { machineId: identity.machineId, projectId };
676
+ },
677
+ grants: async (projectId) => {
678
+ const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
679
+ if (identity === null)
680
+ throw new Error('this machine is not paired');
681
+ const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl, token: identity.token });
682
+ return (await client.sessionGrants(projectId)).grants;
683
+ },
684
+ recordAck: async (projectId, grantId, ack) => {
685
+ const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
686
+ if (identity === null)
687
+ throw new Error('this machine is not paired');
688
+ const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl, token: identity.token });
689
+ await client.acknowledgeSessionGrant(projectId, grantId, ack);
690
+ },
691
+ paths: (sessionId) => (0, session_adapter_runtime_1.adapterPaths)(stateDir, sessionId),
692
+ supportedAdapters: () => SUPPORTED_SESSION_ADAPTERS,
693
+ now: () => new Date().toISOString(),
694
+ note: (line) => process.stdout.write(` ${line}\n`),
695
+ };
696
+ /** What the last measurement left out, so the report can name it. */
697
+ let lastExcluded = [];
698
+ return {
699
+ readClaim: async (path) => {
700
+ try {
701
+ const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
702
+ return isSessionClaimShape(parsed) ? parsed : null;
703
+ }
704
+ catch {
705
+ return null;
706
+ }
707
+ },
708
+ measure: async (claim, consent) => {
709
+ /**
710
+ * ⚠️ THE CHECKOUT IS READ, NOT TRUSTED. `null` on any failure, the same
711
+ * rule `nodeReposDeps` follows: an unreadable tree must reach the fold as
712
+ * absent so it answers `unmeasurable`.
713
+ */
714
+ let workspaceAt = null;
715
+ try {
716
+ workspaceAt = await new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(claim.workspace)).headSha();
717
+ }
718
+ catch {
719
+ workspaceAt = null;
720
+ }
721
+ /** A run this machine already has in that checkout. Exclusive control. */
722
+ let heldBy = null;
723
+ try {
724
+ const holder = new workspace_1.RunRegistry((0, workspace_1.workspacesRoot)(cwd), workspace_1.nodeProbe)
725
+ .active()
726
+ .find((r) => (0, path_1.resolve)(r.workspace) === (0, path_1.resolve)(claim.workspace));
727
+ heldBy = holder === undefined ? null : holder.runId;
728
+ }
729
+ catch {
730
+ heldBy = null;
731
+ }
732
+ /** The product's own liveness probe, with its third answer intact. */
733
+ let liveness;
734
+ try {
735
+ const probed = (0, workspace_1.nodeProbe)(claim.pid);
736
+ liveness = probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
737
+ }
738
+ catch {
739
+ liveness = 'unknown';
740
+ }
741
+ return {
742
+ supportedAdapters: SUPPORTED_SESSION_ADAPTERS,
743
+ repoDeclared: cfg.sessions?.repo !== undefined && cfg.sessions.repo === claim.repo,
744
+ hostIsThisMachine: (0, node_os_1.hostname)() === claim.host,
745
+ liveness,
746
+ workspaceAt,
747
+ workspaceHeldBy: heldBy,
748
+ // Absent config is `null`, NOT an empty allow-list: "nothing is
749
+ // permitted" and "nobody said" are different answers and only one of
750
+ // them is safe to act on.
751
+ scopeAllowed: cfg.sessions?.scope ?? null,
752
+ budgetOwnedHere: cfg.sessions?.budgetOwner === undefined ? null : cfg.sessions.budgetOwner === claim.budgetOwner,
753
+ controlAuthorized: consent.authorized,
754
+ sessionCooperating: consent.acked,
755
+ };
756
+ },
757
+ records: async () => {
758
+ await open();
759
+ return store.all();
760
+ },
761
+ /**
762
+ * 🔴 WHAT THE SERVER AND THE SESSION'S OWN CHANNEL, TOGETHER, SAY.
763
+ *
764
+ * Until this existed, `server.url` and `server.machine_token_file` were
765
+ * parsed by `config.ts` and read by NOBODY, and `sessions --offer` printed
766
+ * "this machine could not ask the server" because nothing in the CLI had
767
+ * ever asked it anything about a grant — a configuration block with no
768
+ * consumer, which is this codebase's most-named defect wearing different
769
+ * clothes.
770
+ *
771
+ * ⚠️ EVERY FAILURE HERE IS `null`, WHICH IS NOT A REFUSAL. Unpaired,
772
+ * unregistered, unreachable: none of them is a decision anybody made about
773
+ * this session, and the command prints them as "nothing was established"
774
+ * rather than as an answer.
775
+ */
776
+ serverControl: (0, session_control_transport_1.serverControlFor)(transportDeps),
777
+ directionContext: (0, session_control_transport_1.directionContextFor)(transportDeps),
778
+ /**
779
+ * 🔴 WRITE THE INSTRUCTION, THEN WAIT — AND BOUND THE WAIT.
780
+ *
781
+ * An unbounded wait on a directory a SESSION writes is this process hanging
782
+ * on the cooperation of the thing it is directing. The bound is short, and
783
+ * running out of it is reported as "no answer yet" rather than as a failure:
784
+ * the instruction was delivered, and what happened next is unknown.
785
+ *
786
+ * ⚠️ THE RESULT FILE IS ARCHIVED, NOT DELETED. It is the only record that
787
+ * the session answered at all.
788
+ */
789
+ direct: async (sessionId, instruction, waitMs) => {
790
+ const paths = (0, session_adapter_runtime_1.adapterPaths)(stateDir, sessionId);
791
+ (0, session_adapter_runtime_1.deliver)(paths, {
792
+ kind: 'instruction',
793
+ instructionId: instruction.id,
794
+ grantId: instruction.grantId,
795
+ verb: instruction.verb,
796
+ args: instruction.args,
797
+ at: instruction.at,
798
+ });
799
+ // F41: `--wait` asks for longer, bounded by `MAX_DIRECT_WAIT_S` where it is parsed.
800
+ const deadline = Date.now() + (waitMs ?? DIRECT_WAIT_MS);
801
+ for (;;) {
802
+ const found = (0, session_adapter_runtime_1.collectResults)(paths, [instruction], new Date().toISOString());
803
+ for (const r of found.results) {
804
+ if (r.ruling.kind === 'accept' && r.ruling.result.instructionId === instruction.id) {
805
+ (0, session_adapter_runtime_1.archiveResult)(paths, r.file);
806
+ return r.ruling.result;
807
+ }
808
+ if (r.ruling.kind === 'refused') {
809
+ process.stdout.write(` ${r.file}: not accepted — ${r.ruling.because}\n`);
810
+ (0, session_adapter_runtime_1.archiveResult)(paths, r.file);
811
+ }
812
+ }
813
+ for (const blind of found.unreadable)
814
+ process.stdout.write(` blind: ${blind}\n`);
815
+ if (Date.now() >= deadline)
816
+ return null;
817
+ await new Promise((resolve) => setTimeout(resolve, DIRECT_POLL_MS));
818
+ }
819
+ },
820
+ append: async (record) => {
821
+ await open();
822
+ await store.append(record);
823
+ },
824
+ now: () => new Date().toISOString(),
825
+ /**
826
+ * The real watcher.
827
+ *
828
+ * ⚠️ `stillAuthorized` IS A CONSTANT FOR AN OBSERVE-ONLY WATCH, AND THE
829
+ * REASON IS WORTH READING RATHER THAN ASSUMING. An observe-only watch has
830
+ * no grant to withdraw — observation was never the half that needed
831
+ * permission — so there is no server answer to ask for and nothing that
832
+ * could come back false. Its bounds are the window's `until`, the session's
833
+ * liveness, the event cap and the stop flag, all of which are real and all
834
+ * of which this runtime honours.
835
+ *
836
+ * 🔴 A CONTROLLED WATCH ASKS FOR REAL (F42). A controlled session's watch
837
+ * outliving its grant is precisely the thing `stopFor` puts revocation
838
+ * first for, and LIVE-3 found this constant doing exactly that: the grant
839
+ * was revoked and the watch recorded on for its whole window. `sessions-cmd`
840
+ * now hands a watch attached under control the server's question as
841
+ * `subject.stillAuthorized`; the constant is only the observe-only default.
842
+ */
843
+ watch: async (window, subject) => {
844
+ const { workspace, pid, stillAuthorized } = subject;
845
+ let stopped = false;
846
+ const onSignal = () => {
847
+ stopped = true;
848
+ };
849
+ process.once('SIGINT', onSignal);
850
+ process.once('SIGTERM', onSignal);
851
+ try {
852
+ return await (0, session_observe_runtime_1.watchSession)(window, { ...session_observe_runtime_1.DEFAULT_WATCH_OPTIONS, root: workspace }, {
853
+ now: () => new Date().toISOString(),
854
+ liveness: () => {
855
+ // The product's own probe, with its third answer intact: an
856
+ // unanswerable pid is `unknown`, and `stopFor` does not end a
857
+ // watch on it.
858
+ try {
859
+ const probed = (0, workspace_1.nodeProbe)(pid);
860
+ return probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
861
+ }
862
+ catch {
863
+ return 'unknown';
864
+ }
865
+ },
866
+ // F42: a controlled watch re-asks the server (`sessions-cmd` hands the
867
+ // question over); an observe-only one has no grant to withdraw.
868
+ stillAuthorized: stillAuthorized ?? (async () => true),
869
+ /**
870
+ * 🔴 AN ABSOLUTE PATH, NEVER THE BARE NAME, AND A GUARD IN THIS
871
+ * REPOSITORY CAUGHT THE FIRST DRAFT DOING IT WRONG. On Windows the
872
+ * cwd is searched before PATH, and the cwd HERE is the workspace of
873
+ * a session being watched — which is to say a directory whose
874
+ * contents are exactly what this code has no reason to trust. A
875
+ * bare `git` would have let the watched tree choose the program
876
+ * that reads it, in the one place where being watched is supposed
877
+ * to be safe.
878
+ *
879
+ * `trustedGitArgs` and `trustedGitEnv` come along for the same
880
+ * reason they do everywhere else: hooks off, signing off, and the
881
+ * whole `GIT_` namespace built rather than inherited.
882
+ */
883
+ revision: () => {
884
+ const bin = (0, trusted_git_1.gitBinary)();
885
+ if (bin === null)
886
+ return null;
887
+ try {
888
+ return (0, node_child_process_1.execFileSync)(bin, [...(0, trusted_git_1.trustedGitArgs)(), 'rev-parse', 'HEAD'], {
889
+ cwd: workspace,
890
+ env: (0, trusted_git_1.trustedGitEnv)(process.env, workspace),
891
+ encoding: 'utf8',
892
+ stdio: ['ignore', 'pipe', 'ignore'],
893
+ }).trim();
894
+ }
895
+ catch {
896
+ return null;
897
+ }
898
+ },
899
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
900
+ askedToStop: () => stopped,
901
+ });
902
+ }
903
+ finally {
904
+ process.removeListener('SIGINT', onSignal);
905
+ process.removeListener('SIGTERM', onSignal);
906
+ }
907
+ },
908
+ /**
909
+ * 🔴 WHAT THIS MACHINE CAN ESTABLISH ABOUT A WORKSPACE IT DOES NOT CONTROL.
910
+ *
911
+ * ⚠️ THE LIVENESS PROBE KEEPS ITS THIRD ANSWER. `unknown` becomes a
912
+ * checkpoint labelled `unknown` rather than one labelled `still`, because a
913
+ * capture whose quality nobody established must not read as the reassuring
914
+ * one.
915
+ */
916
+ recordObservation: async (record) => {
917
+ (0, session_observation_store_1.writeObservation)((0, session_observation_store_1.observationPaths)(stateDir, record.sessionId), record);
918
+ },
919
+ observations: async (sessionId) => {
920
+ const lines = [];
921
+ (0, session_observation_store_1.reportObservations)(sessionId, (0, session_observation_store_1.readObservations)((0, session_observation_store_1.observationPaths)(stateDir, sessionId)), (l) => lines.push(l));
922
+ return lines;
923
+ },
924
+ measureWorkspace: async (workspace, _sessionId, pid) => {
925
+ let liveness;
926
+ try {
927
+ const probed = (0, workspace_1.nodeProbe)(pid);
928
+ liveness = probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
929
+ }
930
+ catch {
931
+ liveness = 'unknown';
932
+ }
933
+ /** A managed run already writing there. A checkpoint of that is a capture of two authors. */
934
+ let heldBy = null;
935
+ try {
936
+ const holder = new workspace_1.RunRegistry((0, workspace_1.workspacesRoot)(cwd), workspace_1.nodeProbe)
937
+ .active()
938
+ .find((r) => (0, path_1.resolve)(r.workspace) === (0, path_1.resolve)(workspace));
939
+ heldBy = holder === undefined ? null : holder.runId;
940
+ }
941
+ catch {
942
+ heldBy = null;
943
+ }
944
+ const reading = (0, session_checkpoint_runtime_1.measureWorkspace)(workspace, liveness, heldBy);
945
+ // Carried to the report, which is the only place a person sees it.
946
+ lastExcluded = reading.excluded;
947
+ return reading;
948
+ },
949
+ writeCheckpoint: async (checkpoint) => {
950
+ const written = (0, session_checkpoint_runtime_1.writeCheckpoint)(stateDir, checkpoint);
951
+ const lines = [];
952
+ (0, session_checkpoint_runtime_1.reportCheckpoint)(written, checkpoint, (l) => lines.push(l), lastExcluded);
953
+ return lines;
954
+ },
955
+ newHandshakeId: () => `hs_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`,
956
+ out: (l) => process.stdout.write(l + '\n'),
957
+ err: (l) => process.stderr.write(l + '\n'),
958
+ };
959
+ }
960
+ /** The adapter contract, checked before anything reads the claim as a claim. */
961
+ function isSessionClaimShape(v) {
962
+ if (typeof v !== 'object' || v === null)
963
+ return false;
964
+ const o = v;
965
+ const strings = ['sessionId', 'adapter', 'adapterVersion', 'repo', 'workspace', 'host', 'sourceSha', 'budgetOwner'];
966
+ if (!strings.every((k) => typeof o[k] === 'string'))
967
+ return false;
968
+ if (typeof o['pid'] !== 'number' || !Number.isInteger(o['pid']))
969
+ return false;
970
+ return Array.isArray(o['scope']) && o['scope'].every((x) => typeof x === 'string');
971
+ }
972
+ /**
973
+ * WHICH DECLARED REPOSITORY IS THIS ONE — by path, never by name.
974
+ *
975
+ * ⚠️ `path` is the only thing that says "this is me". The id is an identity a
976
+ * compatible set names and carries no location, and `name` in `project:` is a
977
+ * different namespace again — matching on either would make a project that
978
+ * happened to share a word with a sibling declare itself circular.
979
+ *
980
+ * Returns null when nothing declared resolves to here, which the fold reads as
981
+ * "not established" rather than "no need is circular".
982
+ */
983
+ /**
984
+ * Why this run may not start yet, when this project declares cross-repo needs.
985
+ *
986
+ * 🔴 THE CHEAP HALF OF ENFORCEMENT, ASKED WHERE THE DIRTY-TREE CHECK IS ASKED.
987
+ * A need is about publication in ANOTHER repository, so answering it costs a
988
+ * `git rev-parse` per declared repo and a read of the local set records — no
989
+ * server, no model. Asking it here means a project whose dependency has not
990
+ * published does not spend a compile and a verify finding that out, which is
991
+ * exactly the waste `fe9f746` removed for a dirty tree.
992
+ *
993
+ * ⚠️ IT IS PROJECT-LEVEL, LIKE THE NEEDS THEMSELVES. It answers "may this
994
+ * project start work at all", never "may this item" — item-level needs are
995
+ * UNFINISHED and this does not pretend otherwise.
996
+ *
997
+ * ⚠️ AN UNREADABLE SIBLING BLOCKS, and that is the safe direction: the fold
998
+ * answers `unmeasured`, and starting dependent work on the strength of "I could
999
+ * not look" is the mistake every other fold in this package refuses.
1000
+ *
1001
+ * Returns null when the project declares no needs, or when every need is met.
1002
+ */
1003
+ /**
1004
+ * Where every declared repository stands, and what was accepted together.
1005
+ *
1006
+ * 🔴 ONE BUILDER, TWO READERS. The project-level gate below asks it whether this
1007
+ * project may start at all; `pickableItems` asks it, through the run, whether
1008
+ * one ITEM's needs are met. Two copies of "what has published" would eventually
1009
+ * answer differently about the same moment, and the one that mattered would be
1010
+ * whichever the reader happened to call.
1011
+ *
1012
+ * ⚠️ IT DOES NOT SHORT-CIRCUIT ON "no project-level needs". That early return
1013
+ * belongs to the gate, not here: a project declaring none of its own can still
1014
+ * hold items that declare theirs, and a builder that gave up early would hand
1015
+ * the run an undefined context and block every one of them as `undeclared`.
1016
+ */
1017
+ async function crossRepoContext(cwd, cfg) {
1018
+ const declared = cfg.repos ?? [];
1019
+ if (declared.length === 0)
1020
+ return undefined;
1021
+ const current = new Map();
1022
+ for (const repo of declared) {
1023
+ try {
1024
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(repo.path));
1025
+ const branches = await git.listBranches(integration_1.INTEGRATION_BRANCH);
1026
+ const integration = branches.find((b) => b.name === integration_1.INTEGRATION_BRANCH);
1027
+ current.set(repo.id, integration === undefined ? await git.headSha() : integration.sha);
1028
+ }
1029
+ catch {
1030
+ // Left out of the map on purpose: absent is what the folds read as
1031
+ // "could not be read", and a fabricated sha would answer a different
1032
+ // question than the one asked.
1033
+ }
1034
+ }
1035
+ const store = new compatible_sets_1.CompatibleSetStore((0, git_driver_1.projectStateDir)(cwd), ledger_store_1.nodeLedgerFs);
1036
+ await store.open();
1037
+ const records = store.all();
1038
+ const setId = (0, compatible_sets_1.newestSetId)(records);
1039
+ return {
1040
+ declared: declared.map((d) => d.id),
1041
+ self: (0, self_repo_id_1.selfRepoId)(declared, cwd),
1042
+ set: setId === null ? null : (0, compatible_sets_1.setFrom)(records, setId),
1043
+ current,
1044
+ };
1045
+ }
1046
+ async function crossRepoNeedsTrouble(configPath) {
1047
+ let cfg;
1048
+ try {
1049
+ cfg = loadConfig(configPath);
1050
+ }
1051
+ catch {
1052
+ // Not this check's business. The run loads the same file a moment later and
1053
+ // reports a bad config far better than a guess from here could.
1054
+ return null;
1055
+ }
1056
+ const needs = cfg.crossRepoNeeds ?? [];
1057
+ if (needs.length === 0)
1058
+ return null;
1059
+ const cwd = process.cwd();
1060
+ const declared = cfg.repos ?? [];
1061
+ const current = new Map();
1062
+ for (const repo of declared) {
1063
+ try {
1064
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(repo.path));
1065
+ const branches = await git.listBranches(integration_1.INTEGRATION_BRANCH);
1066
+ const integration = branches.find((b) => b.name === integration_1.INTEGRATION_BRANCH);
1067
+ current.set(repo.id, integration === undefined ? await git.headSha() : integration.sha);
1068
+ }
1069
+ catch {
1070
+ // Left out of the map on purpose: absent is what the fold reads as
1071
+ // "could not be read", and a fabricated sha would answer a different
1072
+ // question than the one asked.
1073
+ }
1074
+ }
1075
+ const store = new compatible_sets_1.CompatibleSetStore((0, git_driver_1.projectStateDir)(cwd), ledger_store_1.nodeLedgerFs);
1076
+ await store.open();
1077
+ const records = store.all();
1078
+ const setId = (0, compatible_sets_1.newestSetId)(records);
1079
+ const blockers = (0, core_1.crossRepoBlockedBy)({
1080
+ needs,
1081
+ declared: declared.map((d) => d.id),
1082
+ self: (0, self_repo_id_1.selfRepoId)(declared, cwd),
1083
+ set: setId === null ? null : (0, compatible_sets_1.setFrom)(records, setId),
1084
+ current,
1085
+ });
1086
+ return (0, core_1.describeCrossRepoBlockers)(blockers);
1087
+ }
1088
+ function nodeReposDeps() {
1089
+ const cwd = process.cwd();
1090
+ const cfg = loadConfig((0, path_1.join)(cwd, git_driver_1.STATE_DIR, 'config.yaml'));
1091
+ const stateDir = (0, git_driver_1.projectStateDir)(cwd);
1092
+ const store = new compatible_sets_1.CompatibleSetStore(stateDir, ledger_store_1.nodeLedgerFs);
1093
+ let opened = false;
1094
+ const open = async () => {
1095
+ if (!opened) {
1096
+ await store.open();
1097
+ opened = true;
1098
+ }
1099
+ };
1100
+ return {
1101
+ declared: () => cfg.repos ?? [],
1102
+ self: () => (0, self_repo_id_1.selfRepoId)(cfg.repos ?? [], cwd),
1103
+ needs: () => cfg.crossRepoNeeds ?? [],
1104
+ // ⚠️ SHARED WITH THE DAEMON'S INTEGRATOR rather than written twice. Two
1105
+ // copies of "where is this repository" are two copies that can disagree,
1106
+ // and the one that disagrees is the one deciding whether a set drifted.
1107
+ revisionOf: publication_recorder_1.nodeRevisionOf,
1108
+ records: async () => {
1109
+ await open();
1110
+ return store.all();
1111
+ },
1112
+ append: async (record) => {
1113
+ await open();
1114
+ await store.append(record);
1115
+ },
1116
+ now: () => new Date().toISOString(),
1117
+ // `newId`'s prefixes are a closed vocabulary of DB entities; a compatible
1118
+ // set is not one of those, so it is generated here rather than widening it.
1119
+ newId: () => `set_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`,
1120
+ out: (l) => process.stdout.write(l + '\n'),
1121
+ err: (l) => process.stderr.write(l + '\n'),
1122
+ };
1123
+ }
1124
+ async function goals() {
1125
+ const read = await (0, goal_view_1.readGoalsList)(process.cwd(), { now: () => Date.now() });
1126
+ if (!read.ok) {
1127
+ process.stderr.write(`goals : ${read.because}\n`);
1128
+ return read.code;
1129
+ }
1130
+ process.stdout.write((0, goal_view_1.renderGoalsList)(read.entries));
1131
+ return 0;
1132
+ }
1133
+ const DEFAULT_SERVER = 'https://looply-api-607184ed6acc.herokuapp.com';
1134
+ /** Pair this machine with a phone, so runs can be watched and driven remotely. */
1135
+ async function pair(argv) {
1136
+ const at = argv.indexOf('--server');
1137
+ const serverUrl = at >= 0 && argv[at + 1] ? argv[at + 1] : DEFAULT_SERVER;
1138
+ const nameAt = argv.indexOf('--name');
1139
+ const machineName = nameAt >= 0 ? argv[nameAt + 1] : undefined;
1140
+ return (0, daemon_cmd_1.pairCommand)({
1141
+ cwd: process.cwd(),
1142
+ serverUrl,
1143
+ ...(machineName ? { machineName } : {}),
1144
+ log: (line) => process.stdout.write(`${line}\n`),
1145
+ });
1146
+ }
1147
+ /** Keep this machine connected: presence, and shipping the ledger. */
1148
+ async function daemon(argv) {
1149
+ const at = argv.indexOf('--config');
1150
+ const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
1151
+ if (!configPath) {
1152
+ process.stderr.write('daemon: --config needs a path\n');
1153
+ return 1;
1154
+ }
1155
+ // Registering is irreversible, so an unknown directory is asked about. The
1156
+ // asker is only wired up when there is a terminal to answer on: with stdin
1157
+ // piped — a service unit, a CI job — a prompt is an indefinite hang, and the
1158
+ // gate refuses instead, telling you about --yes.
1159
+ const interactive = Boolean(process.stdin.isTTY);
1160
+ return (0, daemon_cmd_1.daemonCommand)({
1161
+ cwd: process.cwd(),
1162
+ config: loadConfig(configPath),
1163
+ once: argv.includes('--once'),
1164
+ log: (line) => process.stdout.write(`${line}\n`),
1165
+ gate: {
1166
+ assumeYes: argv.includes('--yes') || argv.includes('-y'),
1167
+ ...(interactive ? { ask: (0, daemon_cmd_1.nodeRegisterAsk)((s) => process.stdout.write(s)) } : {}),
1168
+ },
1169
+ });
1170
+ }
1171
+ /** Seed or top up the queue by reading the code through several narrow lenses. */
1172
+ async function scan(argv) {
1173
+ const at = argv.indexOf('--config');
1174
+ const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
1175
+ if (!configPath) {
1176
+ process.stderr.write('scan: --config needs a path\n');
1177
+ return 1;
1178
+ }
1179
+ const onlyAt = argv.indexOf('--lens');
1180
+ const only = onlyAt >= 0 && argv[onlyAt + 1] ? argv[onlyAt + 1].split(',') : undefined;
1181
+ const perAt = argv.indexOf('--per-lens');
1182
+ let perLens;
1183
+ if (perAt >= 0) {
1184
+ const n = Number(argv[perAt + 1]);
1185
+ if (!Number.isInteger(n) || n < 1) {
1186
+ process.stderr.write('scan: --per-lens needs a positive whole number\n');
1187
+ return 1;
1188
+ }
1189
+ perLens = n;
1190
+ }
1191
+ const cfg = loadConfig(configPath);
1192
+ try {
1193
+ await (0, scan_cmd_1.scanProject)({
1194
+ cwd: process.cwd(),
1195
+ config: cfg,
1196
+ ...(only ? { only } : {}),
1197
+ ...(perLens !== undefined ? { perLens } : {}),
1198
+ dryRun: argv.includes('--dry-run'),
1199
+ log: (line) => process.stdout.write(`${line}\n`),
1200
+ });
1201
+ }
1202
+ catch (e) {
1203
+ /**
1204
+ * ⚠️ A REFUSAL, NOT A CRASH. Writing the queue while a run holds it loses
1205
+ * the items silently; this says so and exits 2, the way every other
1206
+ * "somebody else is here" answer in this file does.
1207
+ */
1208
+ if (e instanceof queue_write_lease_1.QueueBusy) {
1209
+ process.stderr.write(`\n scan: ${e.message}\n\n`);
1210
+ return 2;
1211
+ }
1212
+ throw e;
1213
+ }
1214
+ return 0;
1215
+ }
1216
+ /**
1217
+ * Check the drafts a scan produced.
1218
+ *
1219
+ * A run does this by itself when it has nothing pickable, so this command is
1220
+ * for doing it deliberately: it is the cheapest agent work looply does, and
1221
+ * seeing what survives contact with the code is worth knowing before spending
1222
+ * a round on anything.
1223
+ */
1224
+ /**
1225
+ * THE INTEGRATION QUEUE'S PRODUCTION ENTRY POINT.
1226
+ *
1227
+ * ⚠️ NOT A SECOND SCHEDULER. It decides nothing about which goal runs; it
1228
+ * offers ONE finished branch, once, and answers whether it went in. The
1229
+ * coordinator still schedules, and this is the seam it — or a person — calls
1230
+ * when a run has finished and its work is ready to converge.
1231
+ *
1232
+ * ⚠️ GATED ON `run.integration`, like everything else in this phase. With the
1233
+ * flag off it says so and changes nothing.
1234
+ */
1235
+ async function integrate(argv) {
1236
+ const cwd = process.cwd();
1237
+ const at = argv.indexOf('--config');
1238
+ const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
1239
+ if (configPath === undefined) {
1240
+ process.stderr.write('integrate: --config needs a path\n');
1241
+ return 1;
1242
+ }
1243
+ const cfg = loadConfig(configPath);
1244
+ const enabled = cfg.run.integration === true;
1245
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(cwd));
1246
+ const initAt = argv.indexOf('--init');
1247
+ if (initAt >= 0) {
1248
+ const from = argv[initAt + 1];
1249
+ if (from === undefined || from.startsWith('--')) {
1250
+ process.stderr.write('integrate: --init needs the ref to start integration from, and it is not guessed — ' +
1251
+ 'that is where every future run will begin\n');
1252
+ return 1;
1253
+ }
1254
+ const out = await (0, integration_runtime_1.initialiseIntegration)(cwd, from, git);
1255
+ if (!out.ok) {
1256
+ process.stderr.write(`integrate: ${out.because}\n`);
1257
+ return 1;
1258
+ }
1259
+ process.stdout.write(out.already
1260
+ ? `integrate: ${out.ref} already exists at ${out.sha.slice(0, 7)}\n`
1261
+ : `integrate: created ${out.ref} at ${out.sha.slice(0, 7)} (from ${from})\n`);
1262
+ return 0;
1263
+ }
1264
+ const branchAt = argv.indexOf('--branch');
1265
+ const branch = branchAt >= 0 ? argv[branchAt + 1] : undefined;
1266
+ if (branch === undefined || branch.startsWith('--')) {
1267
+ process.stderr.write('integrate: --branch needs the branch to offer\n');
1268
+ return 1;
1269
+ }
1270
+ const goalAt = argv.indexOf('--goal');
1271
+ const goalId = goalAt >= 0 ? argv[goalAt + 1] : undefined;
1272
+ /**
1273
+ * THE REQUIRED CHECKS, RUN AGAINST THE CANDIDATE IN ITS OWN CHECKOUT.
1274
+ *
1275
+ * 🔴 A CLEAN FAST-FORWARD IS NOT ACCEPTANCE. These are the project's own
1276
+ * gates — the same ones a round must pass — executed at the commit that would
1277
+ * actually be published, in a checkout cut for the purpose.
1278
+ */
1279
+ // The lane's deps, built once for this command and the daemon (M5-1): the project's own gate in the candidate's checkout.
1280
+ /**
1281
+ * WHAT THIS BRANCH'S DELIVERY CLAIMED, AT ITS TIP — resolved exactly as the daemon's offer resolves
1282
+ * it (`integrateOnce`), so a person's `integrate` is held to the same proof: under programme
1283
+ * admission a candidate with no claim gets the strict lane, and one with a claim must pass what it
1284
+ * cites.
1285
+ */
1286
+ const tip = (await git.refExists(branch)) ? await git.mergeBase(branch, branch) : null;
1287
+ // F33: the same containment the daemon's offer applies.
1288
+ const claim = tip === null
1289
+ ? null
1290
+ : (0, lane_claims_1.deliveredClaimFor)(cwd, { branch, commitSha: tip }, await (0, lane_claims_1.deliveriesInHistory)(cwd, tip, (a, b) => git.isAncestor(a, b), (0, integration_runtime_2.readAttempts)(cwd)));
1291
+ const out = await (0, integration_runtime_1.integrationTick)(cwd, { branch, ...(goalId === undefined ? {} : { goalId }), ...(claim === null ? {} : { claim }) }, enabled, (0, integration_deps_1.nodeIntegrationDeps)(cwd, cfg, (line) => process.stdout.write(`${line}\n`), git));
1292
+ switch (out.kind) {
1293
+ case 'off':
1294
+ process.stdout.write('integrate: run.integration is off — nothing to do\n');
1295
+ return 0;
1296
+ case 'uninitialised':
1297
+ process.stderr.write(`integrate: ${out.because}\n`);
1298
+ return 2;
1299
+ case 'idle':
1300
+ process.stdout.write(`integrate: ${out.because}\n`);
1301
+ return 0;
1302
+ case 'published':
1303
+ process.stdout.write(`integrate: published ${branch} → ${out.attempt.published?.ref} ` +
1304
+ `(${String(out.attempt.published?.from).slice(0, 7)}..${out.attempt.published?.to.slice(0, 7)})\n`);
1305
+ return 0;
1306
+ case 'refused':
1307
+ process.stderr.write(`integrate: not published — ${out.attempt.because ?? 'no reason recorded'}\n`);
1308
+ return 2;
1309
+ }
1310
+ }
1311
+ async function verify(argv) {
1312
+ const at = argv.indexOf('--config');
1313
+ const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
1314
+ if (!configPath) {
1315
+ process.stderr.write('verify: --config needs a path\n');
1316
+ return 1;
1317
+ }
1318
+ const maxAt = argv.indexOf('--max');
1319
+ let max;
1320
+ if (maxAt >= 0) {
1321
+ const n = Number(argv[maxAt + 1]);
1322
+ if (!Number.isInteger(n) || n < 1) {
1323
+ process.stderr.write('verify: --max needs a positive whole number\n');
1324
+ return 1;
1325
+ }
1326
+ max = n;
1327
+ }
1328
+ const log = (line) => { process.stdout.write(`${line}\n`); };
1329
+ let report;
1330
+ try {
1331
+ report = await (0, verify_cmd_1.verifyCommand)({
1332
+ cwd: process.cwd(),
1333
+ config: loadConfig(configPath),
1334
+ ...(max === undefined ? {} : { max }),
1335
+ log,
1336
+ });
1337
+ }
1338
+ catch (e) {
1339
+ // See the note in `scan`. A verify pass writes verdicts into queue.json,
1340
+ // and a live run would write its own snapshot back over every one of them.
1341
+ if (e instanceof queue_write_lease_1.QueueBusy) {
1342
+ process.stderr.write(`\n verify: ${e.message}\n\n`);
1343
+ return 2;
1344
+ }
1345
+ throw e;
1346
+ }
1347
+ if (report.checked === 0) {
1348
+ log('verify : nothing to check — no items are waiting to be confirmed.');
1349
+ return 0;
1350
+ }
1351
+ const verdicts = Object.entries(report.byVerdict)
1352
+ .map(([k, n]) => `${n} ${k}`)
1353
+ .join(' · ');
1354
+ log('');
1355
+ log(`verify : ${report.checked} checked — ${verdicts}`);
1356
+ log(`verify : ${report.ready} ready to work on`);
1357
+ if (report.failed)
1358
+ log(`verify : ${report.failed} could not be written back — run it again`);
1359
+ return 0;
1360
+ }
1361
+ /**
1362
+ * Is a run going right now?
1363
+ *
1364
+ * Read from the projection rather than from a process list: the run may be
1365
+ * on another terminal, and status.json is the thing both of them agree on.
1366
+ * A missing or unreadable file means no, which is the safe reading — the
1367
+ * request is written either way and the next run consumes it.
1368
+ */
1369
+ function isRunLive() {
1370
+ try {
1371
+ const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8'));
1372
+ // `live` is top level. It was read as `parsed.run.live` — the shape
1373
+ // `looply agent --json` produces, not the shape LedgerStore writes — so
1374
+ // the answer was always undefined, `looply stop` always said no run was
1375
+ // going, and it never wrote the request. welcome() and agent() both read
1376
+ // the flat field correctly, which is what let the odd one out survive.
1377
+ return parsed?.live === true;
1378
+ }
1379
+ catch {
1380
+ return false;
1381
+ }
1382
+ }
1383
+ /**
1384
+ * The queue, in the terminal.
1385
+ *
1386
+ * The screen exists for the *why*: a queue of fifteen where twelve are stale,
1387
+ * blocked or duplicates is a queue of three, and a count alone hides that.
1388
+ */
1389
+ async function queue(argv) {
1390
+ const store = new queue_store_1.QueueStore((0, path_1.resolve)('.looply/state'), ledger_store_1.nodeLedgerFs);
1391
+ await store.open();
1392
+ for (const line of (0, queue_view_1.renderQueue)(store.list(), { all: argv.includes('--all') })) {
1393
+ process.stdout.write(line + '\n');
1394
+ }
1395
+ return 0;
1396
+ }
1397
+ /**
1398
+ * What this project is running, and what it left behind.
1399
+ *
1400
+ * 🔴 THE REAPER HAD NO WAY IN. Phase 1 shipped `createWorkspace`, `reapVerdict`
1401
+ * and `reapWorkspace` with NO production caller — so workspaces and registry
1402
+ * entries accumulated with nothing able to retire them, and the orphan lifecycle
1403
+ * the phase claimed to have was unreachable code. This is its home.
1404
+ *
1405
+ * Read-only without `--reap`, deliberately: a person should be able to see what
1406
+ * is going on without authorising a deletion in the same breath.
1407
+ */
1408
+ async function runs(argv) {
1409
+ const projectState = (0, git_driver_1.projectStateDir)(process.cwd());
1410
+ const registry = new workspace_1.RunRegistry(projectState);
1411
+ const entries = registry.list();
1412
+ /**
1413
+ * 🔴 THE SLOTS ARE SHOWN, AND THEY USED NOT TO BE.
1414
+ *
1415
+ * The concurrency bound lives in `slot-<n>.json`, and `list()` filters those
1416
+ * out on purpose — counting them would report every run twice. But nothing
1417
+ * else showed them either, so a slot held by a record that had already gone
1418
+ * was invisible: `looply runs` printed "nothing running, and nothing left
1419
+ * behind" while every start refused with "this project already has 1
1420
+ * execution(s) holding a slot". Nothing in the CLI could see it, name it, or
1421
+ * clear it, and the project was wedged with no way out but deleting files by
1422
+ * hand.
1423
+ */
1424
+ const slots = registry.slots();
1425
+ /**
1426
+ * 🔴 AND THE CHECKOUTS NOTHING ACCOUNTS FOR. `--new-workspace` cuts the
1427
+ * worktree BEFORE the run registers, so anything that ends the run in between
1428
+ * — a config error, a dirty tree, `--dry-run`, a crash — left a real checkout
1429
+ * this command could not see and `--reap` could not reach. One per failed
1430
+ * start, accumulating silently. Found by using the command.
1431
+ */
1432
+ const { abandoned, busy } = (0, workspace_1.abandonedWorkspaces)(process.cwd(), registry);
1433
+ /**
1434
+ * 🔴 AND THE CLAIM LOCK, WHICH WAS INVISIBLE TO EVERY COMMAND. It is only
1435
+ * breakable when its owner is PROVEN dead, so one left by a process that is
1436
+ * alive but hung — or whose pid cannot be judged — denied every start of the
1437
+ * project with a message about "another start deciding" and nothing to look at.
1438
+ * That is the same wedge the slot files had before this listed them.
1439
+ */
1440
+ const lock = registry.claimLock();
1441
+ /**
1442
+ * 🔴 A REAL REPOSITORY MAKES THESE ROWS UNREADABLE. Measured against a clone of
1443
+ * looply itself: a workspace path is
1444
+ * `C:\Users\…\.looply\workspaces\399072e3c9a57ef8\ws-20260904070437-dra9`,
1445
+ * and a box row carrying one came out at 170 characters — which wraps twice on
1446
+ * an 80-column terminal and turns a three-row table into nine lines of noise.
1447
+ * Every fixture used until now had a path short enough to hide it.
1448
+ *
1449
+ * The root is the same for every one of them, so it is said ONCE and the rows
1450
+ * carry the part that differs. A `--in` checkout is somewhere else entirely and
1451
+ * keeps its full path, because that is the only place it appears.
1452
+ */
1453
+ const wsRootShown = (0, path_1.resolve)((0, workspace_1.workspacesRoot)(process.cwd()));
1454
+ const shortWs = (w) => {
1455
+ const r = (0, path_1.resolve)(w);
1456
+ return r.toLowerCase().startsWith(`${wsRootShown.toLowerCase()}${path_1.sep}`)
1457
+ ? `…${path_1.sep}${r.slice(wsRootShown.length + 1)}`
1458
+ : r;
1459
+ };
1460
+ if (entries.length === 0 &&
1461
+ slots.length === 0 &&
1462
+ abandoned.length === 0 &&
1463
+ busy.length === 0 &&
1464
+ lock === null) {
1465
+ /**
1466
+ * ⚠️ AND `--release` STILL HAS TO ANSWER. This returned 0 with "nothing
1467
+ * running, and nothing left behind" BEFORE the flag was parsed, so
1468
+ * `looply runs --release anything-at-all` reported success on a clean
1469
+ * project — a command that did nothing, said nothing about it, and exited 0.
1470
+ */
1471
+ if (argv.includes('--release')) {
1472
+ process.stderr.write(`\n nothing is registered for this project, so there is nothing to release\n\n`);
1473
+ return 2;
1474
+ }
1475
+ process.stdout.write(`\n ${(0, ui_1.dim)('nothing running, and nothing left behind')}\n\n`);
1476
+ return 0;
1477
+ }
1478
+ if (entries.length > 0) {
1479
+ const rows = entries.map((e) => e.kind === 'unreadable'
1480
+ ? [
1481
+ 'unreadable',
1482
+ // 🔴 AND HOW TO CLEAR IT. This printed the path and the reason and
1483
+ // stopped, and `--release` then refused the name it had just shown
1484
+ // — the one row a person most wants gone was the one nothing could
1485
+ // remove. It holds no slot, so this is clutter rather than a wedge,
1486
+ // but a listing that offers no way out stops being believed.
1487
+ `${e.path} — ${e.because}; \`looply runs --release ${(0, path_1.basename)(e.path).replace(/\.json$/, '')}\` clears it`,
1488
+ ]
1489
+ : [
1490
+ e.kind === 'active' ? 'running' : e.kind === 'orphaned' ? 'orphaned' : 'unjudgeable',
1491
+ `${e.record.runId} ${e.record.branch} pid ${e.record.pid} ${shortWs(e.record.workspace)}`,
1492
+ ]);
1493
+ process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'executions' })}\n`);
1494
+ }
1495
+ if (lock !== null) {
1496
+ const who = lock.pid === null
1497
+ ? 'nothing this can identify'
1498
+ : `pid ${lock.pid}, which is ${lock.liveness === 'alive' ? 'running' : lock.liveness === 'dead' ? 'gone' : 'not answering'}`;
1499
+ process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)([
1500
+ [
1501
+ 'claim lock',
1502
+ `held by ${who} ${(0, ui_1.dim)(lock.liveness === 'dead' ? 'the next start will break it' : '`looply runs --release claim-lock` clears it')}`,
1503
+ ],
1504
+ ]), { title: 'starting' })}
1505
+ `);
1506
+ }
1507
+ if (slots.length > 0) {
1508
+ const rows = slots.map((s) => [
1509
+ `slot-${s.slot}`,
1510
+ s.record === null
1511
+ ? `${(0, ui_1.warn)('unreadable')} — nothing can say whose it is; \`looply runs --release slot-${s.slot}\` clears it`
1512
+ : `${s.record.runId} pid ${s.record.pid}`,
1513
+ ]);
1514
+ process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'concurrency slots' })}\n`);
1515
+ }
1516
+ /**
1517
+ * 🔴 AND THE FILES THAT LOOK LIKE SLOTS AND ARE NOT.
1518
+ *
1519
+ * `slot-abc.json` and `slot-01.json` are deliberately not slots: neither
1520
+ * name round-trips, so `--release slot-<n>` could not clear them and a row
1521
+ * offering that would be a lie. They were then in no listing at all — a file
1522
+ * plainly on disk, mentioned nowhere, which is precisely the state this
1523
+ * command exists to make impossible. They are inert; they are also printed.
1524
+ */
1525
+ const strays = registry.strayFiles();
1526
+ if (strays.length > 0) {
1527
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — ${strays.length} file(s) in the slots directory nothing can name. ` +
1528
+ `Inert: nothing counts them and nothing reads them. Delete them if you like:\n`);
1529
+ for (const s of strays)
1530
+ process.stdout.write(` ${(0, ui_1.dim)(s)}\n`);
1531
+ process.stdout.write('\n');
1532
+ }
1533
+ if (abandoned.length > 0 || busy.length > 0) {
1534
+ /**
1535
+ * ⚠️ BUSY CHECKOUTS ARE SHOWN TOO. They used to be skipped in silence, so a
1536
+ * real directory on disk existed nowhere: no row, no count, no reason, and
1537
+ * this command printing "nothing running, and nothing left behind". They
1538
+ * are not offered for reaping — something is using them — but a person
1539
+ * looking at a directory that is plainly there deserves to see it.
1540
+ */
1541
+ const rows = [
1542
+ ...busy.map((w) => [
1543
+ 'in use',
1544
+ `${shortWs(w)} ${(0, ui_1.dim)(`in use — \`looply runs --release ${(0, path_1.basename)(w)}\` clears its start marker`)}`,
1545
+ ]),
1546
+ ...abandoned.map((w) => {
1547
+ /**
1548
+ * 🔴 "ABANDONED" AND "KEPT ON PURPOSE" ARE DIFFERENT THINGS, and this
1549
+ * said the first about both. A checkout the reaper has already refused —
1550
+ * because it holds a person's uncommitted files, or because its git
1551
+ * config cannot be read — was listed as "no execution accounts for this
1552
+ * checkout", which is true and reads as "nothing is using this, remove
1553
+ * it". The quarantine record is what lets the row say which it is.
1554
+ */
1555
+ const held = (0, workspace_quarantine_1.quarantineOf)(process.cwd(), w);
1556
+ return held === null
1557
+ ? ['abandoned', `${shortWs(w)} ${(0, ui_1.dim)('no execution accounts for this checkout')}`]
1558
+ : [
1559
+ 'quarantined',
1560
+ `${shortWs(w)} ${(0, ui_1.warn)(held.because)} ${(0, ui_1.dim)(`— \`looply runs --release ${(0, path_1.basename)(w)}\` when you have looked`)}`,
1561
+ ];
1562
+ }),
1563
+ ];
1564
+ process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'workspaces' })}
1565
+ `);
1566
+ process.stdout.write(` ${(0, ui_1.dim)(`… = ${wsRootShown}`)}
1567
+ `);
1568
+ }
1569
+ /**
1570
+ * THE LAST RESORT, AND THE ONLY THING THAT CAN CLEAR A HOLD THAT LOOKS ALIVE.
1571
+ *
1572
+ * Liveness here is a pid, and pids are recycled. A run killed hard leaves both
1573
+ * its registry entry and its lease behind, so every automatic witness agrees
1574
+ * the run is going — and if a later process inherits that pid, they agree for
1575
+ * ever. The slot is never reclaimed and the project's concurrency is one
1576
+ * lower than it says, permanently, with nothing to point at.
1577
+ *
1578
+ * ⚠️ SO A PERSON SAYS SO, BY NAME. Not a flag on `--reap`, not a heuristic
1579
+ * about how old a record is, and nothing automatic: the name has to be typed.
1580
+ * It is the same shape as the one-time explicit release the owner accepted
1581
+ * for F7 — narrow, deliberate, and impossible to reach by accident.
1582
+ *
1583
+ * ⚠️ `slot-<n>` IS ACCEPTED AS WELL AS A runId, because the case this exists
1584
+ * for includes a slot file nothing can read — and a corrupt slot has no runId
1585
+ * to type.
1586
+ *
1587
+ * ⚠️ IT DOES NOT TOUCH THE WORKSPACE. Forgetting a hold is a statement about
1588
+ * bookkeeping; the checkout and everything in it stays exactly where it is,
1589
+ * and `--reap` — which proves death first — is still the only thing that
1590
+ * removes one.
1591
+ */
1592
+ const releaseAt = argv.indexOf('--release');
1593
+ if (releaseAt !== -1) {
1594
+ const name = argv[releaseAt + 1];
1595
+ if (name === undefined || name.startsWith('--')) {
1596
+ process.stderr.write(`\n --release needs the runId or slot-<n> to clear, exactly as listed above\n\n`);
1597
+ return 2;
1598
+ }
1599
+ if (name === 'claim-lock') {
1600
+ /**
1601
+ * ⚠️ WARN WHEN ITS OWNER IS RUNNING. The box a few lines above may have
1602
+ * just said "held by pid N, which is running", and clearing it anyway lets
1603
+ * a second start into a decision another process is in the middle of.
1604
+ */
1605
+ if (lock !== null && lock.liveness !== 'dead') {
1606
+ const how = lock.liveness === 'alive' ? 'answers' : 'cannot be judged';
1607
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${lock.pid ?? '?'} ${how}. If that is a start still deciding, ` +
1608
+ `clearing this lets another one decide alongside it.
1609
+ `);
1610
+ }
1611
+ const cleared = registry.forgetClaimLock();
1612
+ process.stdout.write(cleared
1613
+ ? ` claim-lock: cleared — the next start decides for itself
1614
+
1615
+ `
1616
+ : ` claim-lock: nothing to clear
1617
+
1618
+ `);
1619
+ return cleared ? 0 : 2;
1620
+ }
1621
+ const asSlot = /^slot-(\d+)$/.exec(name);
1622
+ if (asSlot !== null) {
1623
+ const n = Number.parseInt(asSlot[1] ?? '', 10);
1624
+ /**
1625
+ * 🔴 THE SAME WARNING ITS SIBLING GIVES. This branch cleared a slot with
1626
+ * no read and no probe, while the runId branch a few lines below probes
1627
+ * and says "pid N answers, so this may be a real run". The justification
1628
+ * for accepting `slot-<n>` at all is a slot nothing can read — and that
1629
+ * covers only the unreadable case, not one that reads perfectly and names
1630
+ * a process that is running right now.
1631
+ */
1632
+ const holder = slots.find((sl) => sl.slot === n)?.record ?? null;
1633
+ if (holder !== null && (0, workspace_1.nodeProbe)(holder.pid) !== 'dead') {
1634
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — slot-${n} names ${holder.runId}, pid ${holder.pid}, which answers. ` +
1635
+ `Clearing it lets another execution start alongside it.
1636
+ `);
1637
+ }
1638
+ const cleared = registry.forgetSlot(n);
1639
+ process.stdout.write(cleared
1640
+ ? ` slot-${n}: cleared — the run's own record, if it has one, was left alone\n\n`
1641
+ : ` slot-${n}: nothing to clear\n\n`);
1642
+ return cleared ? 0 : 2;
1643
+ }
1644
+ /**
1645
+ * ⚠️ IT MUST BE ONE THIS PROJECT ACTUALLY LISTS. `forget` builds a path from
1646
+ * the name, so a typed `../../something` would otherwise reach outside the
1647
+ * registry. `RunRegistry` refuses that shape itself; this refuses it earlier,
1648
+ * with a sentence about the project rather than about path syntax.
1649
+ */
1650
+ const known = entries.find((e) => e.kind !== 'unreadable' && e.record.runId === name);
1651
+ const holdsSlot = slots.some((s) => s.record?.runId === name);
1652
+ /**
1653
+ * 🔴 AND AN UNREADABLE RECORD IS CLEARABLE BY THE NAME IT IS PRINTED UNDER.
1654
+ *
1655
+ * `known` deliberately skips `unreadable` entries — there is no trustworthy
1656
+ * runId inside a file that will not parse, and `--release` must never act
1657
+ * on one. But the FILENAME is not inside the file: it is `<name>.json` in
1658
+ * a directory this owns, and it is what `looply runs` prints. So the one
1659
+ * entry a person is most likely to want gone was the one entry nothing
1660
+ * could remove: `--release` said "no execution called X is registered" and
1661
+ * `--reap` skipped it, for ever, with the path on screen the whole time.
1662
+ *
1663
+ * It holds no slot and blocks nothing, so this is clutter rather than a
1664
+ * wedge — but a listing that shows a thing and offers no way to clear it is
1665
+ * how a person learns to stop believing the listing.
1666
+ */
1667
+ const unreadableHere = entries.find((e) => e.kind === 'unreadable' && (0, path_1.basename)(e.path) === `${name}.json`);
1668
+ /**
1669
+ * A workspace listed as `in use` whose start marker names a pid nothing can
1670
+ * distinguish from the run that wrote it.
1671
+ *
1672
+ * ⚠️ AFTER THE RECORD LOOKUP, DELIBERATELY. A name that is BOTH a known
1673
+ * runId and a workspace directory clears the RECORD — the more specific
1674
+ * thing, and what somebody reading the executions table means. This branch
1675
+ * used to come first and would silently have done the other one.
1676
+ *
1677
+ * 🔴 AND IT WARNS, BECAUSE THIS MARKER MAY BE THE CHECKOUT'S ONLY
1678
+ * PROTECTION. Between `worktree add` and the run taking its lease there is
1679
+ * no record and no lease — just this file. Clearing it with no probe and no
1680
+ * word let the very next `--reap` remove a live starting run's checkout.
1681
+ */
1682
+ if (known === undefined && unreadableHere === undefined && !holdsSlot && busy.some((w) => (0, path_1.basename)(w) === name)) {
1683
+ const holder = (0, workspace_1.creationMarkerHolder)(process.cwd(), name);
1684
+ if (holder !== null && holder.liveness !== 'dead') {
1685
+ const how = holder.liveness === 'alive' ? 'answers' : 'cannot be judged';
1686
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${holder.pid} ${how}. If that is a run still starting up, ` +
1687
+ `clearing this lets the next \`--reap\` remove its checkout.\n`);
1688
+ }
1689
+ const cleared = (0, workspace_1.forgetCreationMarker)(process.cwd(), name);
1690
+ process.stdout.write(cleared
1691
+ ? ` ${name}: its start marker was cleared — the checkout was left alone\n\n`
1692
+ : ` ${name}: nothing to clear\n\n`);
1693
+ return cleared ? 0 : 2;
1694
+ }
1695
+ /**
1696
+ * 🔴 THE ONLY THING THAT CLEARS A QUARANTINE, and it must be a person.
1697
+ *
1698
+ * A quarantine never expires and nothing re-derives it away: not a clean
1699
+ * tree, not a later run, not a reap. This is the sentence in
1700
+ * `recovery.ts` — "looply cannot tell whose changes those are" — turned into
1701
+ * a command. It removes the RECORD and never the directory, so whatever
1702
+ * made looply refuse is still exactly where the person left it.
1703
+ *
1704
+ * ⚠️ BEFORE THE RECORD BRANCH, AND THAT ORDER IS THE FIX FOR A MEASURED
1705
+ * DATA-LOSS CHAIN. A workspace directory is NAMED for its runId, so
1706
+ * `--release <name>` matches BOTH this and the run-record branch. With the
1707
+ * record first, the person who read a quarantine refusal and typed the
1708
+ * command it printed cleared the RECORD instead — was told "cleared — the
1709
+ * workspace was left alone", which reads as confirmation — and thereby made
1710
+ * the checkout record-less, i.e. abandoned. The next `--reap` then removed
1711
+ * it. Looply's own advice was the shortest route to losing the checkout it
1712
+ * had just refused to touch.
1713
+ *
1714
+ * A person who has typed the name of a QUARANTINED workspace means the
1715
+ * quarantine. The record is left alone, and the message says so.
1716
+ */
1717
+ const quarantinedAt = (0, path_1.join)((0, workspace_1.workspacesRoot)(process.cwd()), name);
1718
+ const held = (0, workspace_quarantine_1.quarantineOf)(process.cwd(), quarantinedAt);
1719
+ if (held !== null) {
1720
+ const gone = (0, workspace_quarantine_1.clearQuarantine)(process.cwd(), quarantinedAt);
1721
+ process.stdout.write(gone
1722
+ ? ` ${name}: quarantine cleared — the checkout was left exactly as it is.\n` +
1723
+ ` ${(0, ui_1.dim)(`it was held because: ${held.because}`)}\n` +
1724
+ (known === undefined
1725
+ ? '\n'
1726
+ : ` ${(0, ui_1.dim)(`its execution record is untouched; \`looply runs --release ${name}\` again clears that`)}\n\n`)
1727
+ : ` ${name}: its quarantine record could not be removed\n\n`);
1728
+ return gone ? 0 : 2;
1729
+ }
1730
+ if (known === undefined && unreadableHere === undefined && !holdsSlot) {
1731
+ process.stderr.write(`\n no execution called ${name} is registered for this project\n\n`);
1732
+ return 2;
1733
+ }
1734
+ if (unreadableHere !== undefined && known === undefined) {
1735
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — nothing can read this record, so nothing can say whose it was. ` +
1736
+ `It holds no slot.\n`);
1737
+ }
1738
+ if (known?.kind === 'active') {
1739
+ process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${known.record.pid} answers, so this may be a real run. ` +
1740
+ `Clearing it lets another execution start alongside it.\n`);
1741
+ }
1742
+ const out = registry.forget(name);
1743
+ process.stdout.write(out.forgot || out.slot !== null
1744
+ ? ` ${name}: cleared${out.slot === null ? '' : ` (slot ${out.slot} freed)`} — the workspace was left alone\n\n`
1745
+ : ` ${name}: nothing to clear\n\n`);
1746
+ return 0;
1747
+ }
1748
+ if (!argv.includes('--reap')) {
1749
+ /**
1750
+ * 🔴 AND IT SAYS WHICH ONES `--reap` CAN ACTUALLY TAKE. This printed
1751
+ * "N orphaned — looply runs --reap retires the ones it can prove are dead"
1752
+ * for every orphan, including the ones it refuses permanently and
1753
+ * correctly: a crash in the main checkout, or in a `--in` checkout a
1754
+ * person chose. Following that advice does nothing, once a day, for ever.
1755
+ */
1756
+ const orphans = entries.filter((e) => e.kind === 'orphaned');
1757
+ /**
1758
+ * 🔴 AND IT COUNTS THEM THE WAY THE REAPER REFUSES THEM.
1759
+ *
1760
+ * This asked `record.worktree`, which is true for ANY linked worktree —
1761
+ * including one a person made and ran `--in`. A crash there was announced
1762
+ * as "in a checkout looply made", and `--reap` then refused it, for ever.
1763
+ * `reapableCheckout` is the reaper's own permanent test, so the sentence
1764
+ * and the command behind it now agree by construction.
1765
+ */
1766
+ const reapable = orphans.filter((e) => e.kind === 'orphaned' && (0, workspace_1.reapableCheckout)(e.record, process.cwd()).ok).length;
1767
+ if (orphans.length > 0) {
1768
+ process.stdout.write(reapable > 0
1769
+ ? ` ${(0, ui_1.dim)(`${orphans.length} orphaned, ${reapable} in a checkout looply made — looply runs --reap`)}\n`
1770
+ : ` ${(0, ui_1.dim)(`${orphans.length} orphaned, none in a checkout looply made — looply runs --release <runId>`)}\n`);
1771
+ }
1772
+ if (abandoned.length > 0) {
1773
+ process.stdout.write(` ${(0, ui_1.dim)(`${abandoned.length} checkout(s) nothing accounts for — \`looply runs --reap\` retires those it can`)}
1774
+ `);
1775
+ }
1776
+ /**
1777
+ * ⚠️ PROVEN DEAD OR UNREADABLE — NOT MERELY "not active". `active()`
1778
+ * deliberately over-counts an unjudgeable pid as running, so counting
1779
+ * anything that is not `active` here made the same screen print
1780
+ * `unjudgeable · run_x pid 4` AND `1 slot(s) held by nothing that is
1781
+ * running`, and then recommend clearing it. A slot nobody can judge is
1782
+ * exactly the one a person should not be nudged into freeing.
1783
+ */
1784
+ const stuck = slots.filter((s) => s.record === null ||
1785
+ entries.some((e) => e.kind === 'orphaned' && e.record.runId === s.record?.runId) ||
1786
+ !entries.some((e) => e.kind !== 'unreadable' && e.record.runId === s.record?.runId)).length;
1787
+ if (stuck > 0) {
1788
+ process.stdout.write(` ${(0, ui_1.dim)(`${stuck} slot(s) held by nothing that is running — \`looply runs --release <runId|slot-N>\` clears one`)}\n`);
1789
+ }
1790
+ process.stdout.write('\n');
1791
+ return 0;
1792
+ }
1793
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(process.cwd()));
1794
+ /**
1795
+ * What looply itself put in those checkouts, so the reaper does not read its
1796
+ * own copy as the person's file and refuse for ever. See `workspaceResidue`.
1797
+ *
1798
+ * Best-effort: a project with no readable config is reaped exactly as before.
1799
+ */
1800
+ let carried = [];
1801
+ try {
1802
+ carried = loadConfig((0, path_1.join)(process.cwd(), git_driver_1.STATE_DIR, 'config.yaml')).run.carryIntoCheckouts ?? [];
1803
+ }
1804
+ catch {
1805
+ /* no config, or an unreadable one: nothing is excused, which is the old behaviour */
1806
+ }
1807
+ for (const w of abandoned) {
1808
+ const out = await (0, workspace_1.reapAbandoned)(git, process.cwd(), w, undefined, undefined, undefined, carried);
1809
+ process.stdout.write(out.reaped
1810
+ ? ` ${(0, path_1.basename)(w)}: retired — no execution accounted for it
1811
+ `
1812
+ : ` ${(0, path_1.basename)(w)}: ${(0, ui_1.warn)('kept')} — ${out.because ?? 'unknown'}
1813
+ `);
1814
+ }
1815
+ for (const e of entries) {
1816
+ if (e.kind !== 'orphaned')
1817
+ continue;
1818
+ const out = await (0, workspace_1.reapWorkspace)(git, registry, e.record, process.cwd(), undefined, undefined, undefined, carried);
1819
+ process.stdout.write(out.reaped
1820
+ ? ` ${e.record.runId}: retired${out.evidenceAt === undefined ? '' : ` — its records kept at ${out.evidenceAt}`}\n`
1821
+ : ` ${e.record.runId}: ${(0, ui_1.warn)('kept')} — ${out.because ?? 'unknown'}` +
1822
+ /**
1823
+ * ⚠️ AND SAY WHAT TO DO ABOUT THE ONE KIND `--reap` CAN NEVER CLEAR.
1824
+ * A run that crashed in the MAIN checkout leaves a record whose
1825
+ * workspace no execution created, so this refuses it every time,
1826
+ * correctly and for ever. Without this a person is told 'kept' once a
1827
+ * day with no way forward.
1828
+ */
1829
+ /**
1830
+ * ⚠️ EVERY REFUSAL `--reap` CANNOT RESOLVE BY RETRYING, not only the
1831
+ * main-checkout one. A `--in` checkout a person chose is refused for
1832
+ * exactly the same reason and just as permanently — looply did not
1833
+ * make it, so looply may not remove it — and without this the person
1834
+ * is nagged about it once a day with no way forward.
1835
+ *
1836
+ * A refusal about FILES is deliberately excluded: that one resolves
1837
+ * itself when the person moves or deletes them, and pointing them at
1838
+ * `--release` instead would be telling them to hide it.
1839
+ */
1840
+ (out.because?.includes('workspaces root') === true ||
1841
+ out.because?.includes('main checkout') === true
1842
+ ? ` — looply runs --release ${e.record.runId} clears the record`
1843
+ : '') +
1844
+ '\n');
1845
+ }
1846
+ process.stdout.write('\n');
1847
+ return 0;
1848
+ }
1849
+ /**
1850
+ * Ask a running loop to stop.
1851
+ *
1852
+ * The request goes to control.json and the run reads it between rounds, which
1853
+ * is also why --now kills nothing: a round interrupted halfway leaves a
1854
+ * half-written tree, and the next run refuses to start on one. The round in
1855
+ * flight always finishes.
1856
+ */
1857
+ /**
1858
+ * 🔴 `pause` AND `resume`: TWO VERBS THE DAEMON HAS ALWAYS WRITTEN AND THE
1859
+ * TERMINAL COULD NEVER SAY.
1860
+ *
1861
+ * `ControlKind` has had all three since it was written, the run reads all
1862
+ * three, and the phone can send all three — but the CLI only ever wrote `stop`.
1863
+ * So a person at the machine had exactly one brake with one setting: end the
1864
+ * run. Wanting it to hold for ten minutes while they looked at something meant
1865
+ * stopping it and starting again, which spends a Start and loses the queue's
1866
+ * position.
1867
+ *
1868
+ * ⚠️ THE SAME FANNING WRITER AS `stop`, deliberately. A pause that reached the
1869
+ * first execution to look and no others would be exactly the defect Phase 1
1870
+ * measured and fixed for `stop` — and it would be silent, because a paused run
1871
+ * and a running one look identical from outside.
1872
+ */
1873
+ /**
1874
+ * ⚠️ AND IT PASSES ARGV THROUGH. It used to hand `stop` an empty array, so
1875
+ * `looply pause --now` silently dropped the flag that `stop` exists to refuse
1876
+ * loudly — the one case where a person believes they did something drastic.
1877
+ */
1878
+ async function pauseOrResume(kind, argv) {
1879
+ return stop(argv, kind);
1880
+ }
1881
+ async function stop(argv, kind = 'stop') {
1882
+ /**
1883
+ * 🔴 THE BRAKE HAS TO ADDRESS THE PROJECT, NOT A DIRECTORY.
1884
+ *
1885
+ * Phase 1 moved the run's `ControlStore` to the PROJECT state directory — so
1886
+ * that one stop would reach every workspace — and left this writer at the cwd.
1887
+ * The two then pointed at different files whenever a run was executing
1888
+ * anywhere but the main checkout: from the main checkout `isRunLive` found no
1889
+ * `status.json` and this wrote NOTHING while the run carried on, and from
1890
+ * inside a workspace it wrote a control file the run does not read. A human
1891
+ * brake that silently does nothing is worse than no brake at all.
1892
+ *
1893
+ * Liveness comes from the registry for the same reason. `status.json` is
1894
+ * execution-scoped by design, so asking one directory whether "a run" is going
1895
+ * cannot answer a question about N of them — and the registry probes the pid
1896
+ * rather than trusting a file.
1897
+ *
1898
+ * ⚠️ SIMPLE MODE IS UNCHANGED: in an ordinary checkout the project and
1899
+ * execution directories are the same path, and `isRunLive` still answers.
1900
+ */
1901
+ const projectState = (0, git_driver_1.projectStateDir)(process.cwd());
1902
+ const running = new workspace_1.RunRegistry(projectState).active();
1903
+ const live = running.length > 0 || isRunLive();
1904
+ /**
1905
+ * ⚠️ ONE WRITER, SHARED WITH THE DAEMON. The shared `control.json` plus one
1906
+ * addressed request per live execution — see `requestControlEverywhere`. The
1907
+ * daemon had the same hole and a different code path; now it has neither.
1908
+ */
1909
+ let addressed = [];
1910
+ let unreachable = [];
1911
+ /**
1912
+ * 🔴 A STANDING HOLD OUTLIVES THE RUN, SO `resume` MUST WRITE WITH NOTHING
1913
+ * RUNNING — AND IT DID NOT. THIS WAS A DEADLOCK WITH NO KEY.
1914
+ *
1915
+ * MEASURED: `looply pause` during a run, run ends, `control.json` still reads
1916
+ * `pause`. The daemon's autonomy tick reads that same project file to answer
1917
+ * "has a person asked this machine to hold" — its own docblock says so — so
1918
+ * the machine holds. `looply resume` then printed "no run is going — nothing
1919
+ * to resume", wrote NOTHING, exited 0, and left the hold exactly where it
1920
+ * was. The one documented way out of a hold could not clear one.
1921
+ *
1922
+ * ⚠️ `pause` IS THE SAME FACT FROM THE OTHER SIDE. Asking the machine to hold
1923
+ * BEFORE it starts anything is a coherent thing to want, and the daemon reads
1924
+ * the file that expresses it. Refusing to write it because nothing is running
1925
+ * yet is refusing the request at exactly the moment it is cheapest to honour.
1926
+ *
1927
+ * ⚠️ `stop` IS DIFFERENT AND STAYS DIFFERENT. It addresses a RUN. There is no
1928
+ * such thing as stopping a run that does not exist, and writing a standing
1929
+ * `stop` would leave an order that the next run consumes on its way past —
1930
+ * which is the "last night's stop wedges tonight's run" hazard the old
1931
+ * comment here was rightly worried about.
1932
+ */
1933
+ const standing = kind !== 'stop';
1934
+ /** Whether a control file was actually written. Every message below reads this. */
1935
+ const wrote = live || standing;
1936
+ if (wrote) {
1937
+ const out = await (0, workspace_1.requestControlEverywhere)(projectState, running, kind, 'human:cli', ledger_store_1.nodeLedgerFs, process.cwd());
1938
+ addressed = out.reached;
1939
+ unreachable = out.unreachable;
1940
+ }
1941
+ if (running.length > 1) {
1942
+ /**
1943
+ * ⚠️ THE VERB IS THE ONE THE PERSON TYPED. This line was written for `stop`
1944
+ * and then reused by `pause` and `resume` without being read again, so
1945
+ * `looply resume` on three executions reported that three executions "were
1946
+ * told to stop" — the exact opposite of what had just been asked for, in
1947
+ * the one place a person looks to check the brake reached everything.
1948
+ */
1949
+ const told = kind === 'resume' ? 'were told to carry on' : `were told to ${kind}`;
1950
+ process.stdout.write(`
1951
+ ${(0, ui_1.dim)(`${addressed.length} of ${running.length} running executions ${told}.`)}
1952
+ `);
1953
+ }
1954
+ if (unreachable.length > 0) {
1955
+ process.stdout.write(` ${(0, ui_1.warn)('could not reach')} ${unreachable.join(', ')} — their state directories could not be written
1956
+ `);
1957
+ }
1958
+ process.stdout.write('\n ' + (0, queue_view_1.stopMessage)(live, kind) + '\n');
1959
+ /**
1960
+ * 🔴 `--now` IS REMOVED RATHER THAN IMPLEMENTED, AND THAT IS THE HONEST END
1961
+ * OF A FLAG THAT NEVER DID ANYTHING.
1962
+ *
1963
+ * It was parsed, it printed a sentence, and it changed nothing — while
1964
+ * `DESIGN.md` advertised "graceful … or immediate". A flag that is accepted
1965
+ * and ignored is worse than one that does not exist: a person who types it in
1966
+ * an emergency believes they have done something.
1967
+ *
1968
+ * And it cannot be implemented, deliberately. Phase 3 refused to add a hard
1969
+ * cancel because "a round in flight always finishes" is the invariant
1970
+ * RECOVERY rests on — cut one off and a dirty tree becomes looply's own
1971
+ * mid-round state, which the next run cannot tell from a person's edits. The
1972
+ * flag was advertising a capability the architecture is built to refuse.
1973
+ *
1974
+ * ⚠️ IT IS REFUSED LOUDLY, NOT IGNORED QUIETLY. Somebody's script may pass it.
1975
+ */
1976
+ if (argv.includes('--now')) {
1977
+ process.stdout.write('\n ' +
1978
+ (0, ui_1.warn)('--now was removed') +
1979
+ ' — it never did anything. A round in flight always finishes, because\n' +
1980
+ ' cutting one off leaves a half-written tree the next run cannot tell from your edits.\n' +
1981
+ /**
1982
+ * 🔴 IT REPORTS WHAT WAS WRITTEN, FROM THE VARIABLE THAT DECIDED IT —
1983
+ * AND THE FIRST TWO VERSIONS OF THIS LINE BOTH LIED, IN OPPOSITE
1984
+ * DIRECTIONS.
1985
+ *
1986
+ * The original said "The stop above is in effect" unconditionally, so
1987
+ * `looply stop --now` with nothing running printed "no run is going"
1988
+ * and then, two lines later, the loudest possible reassurance
1989
+ * contradicting it. The repair replaced that with "nothing was
1990
+ * written" keyed on `live` — and by then `pause` and `resume` DID
1991
+ * write with nothing running, so `looply pause --now` announced that
1992
+ * nothing had been written over a control file it had just written.
1993
+ *
1994
+ * One false sentence about a write replaced by another is not a
1995
+ * repair; it is the same defect with a new value. So the sentence is
1996
+ * derived from `wrote`, which is the variable the write itself is
1997
+ * conditioned on, and the two cannot disagree without the compiler
1998
+ * noticing.
1999
+ */
2000
+ (wrote
2001
+ ? ` The ${kind} above is in effect.\n`
2002
+ : ' There was nothing running to stop, so nothing was written.\n'));
2003
+ }
2004
+ process.stdout.write('\n');
2005
+ return 0;
2006
+ }
2007
+ /**
2008
+ * Start a run.
2009
+ *
2010
+ * `--dry-run` prints the plan and calls no agent, which is how you find out the
2011
+ * queue is empty or proof is switched off in a second rather than at round one
2012
+ * of an overnight run.
2013
+ */
2014
+ async function run(argv) {
2015
+ /**
2016
+ * WHERE THIS EXECUTION RUNS. Absent means here, which is Simple Mode.
2017
+ *
2018
+ * `--in` is the whole of Phase 1's parallel interface, and that is deliberate:
2019
+ * a second isolated execution is a second `looply run` in a second workspace,
2020
+ * and looply already knows how to be one run in one checkout. Nothing about
2021
+ * the no-flag path changes — same cwd, same state directory, same lease.
2022
+ */
2023
+ const inAt = argv.indexOf('--in');
2024
+ let workspace;
2025
+ if (inAt >= 0) {
2026
+ const raw = argv[inAt + 1];
2027
+ if (raw === undefined || raw.startsWith('--')) {
2028
+ process.stderr.write('run: --in needs a path\n');
2029
+ return 1;
2030
+ }
2031
+ workspace = (0, path_1.resolve)(raw);
2032
+ if (!(0, fs_1.existsSync)(workspace)) {
2033
+ process.stderr.write(`run: no such directory: ${workspace}\n`);
2034
+ return 1;
2035
+ }
2036
+ /**
2037
+ * The same refusal every other verb gets, at the earliest point a person
2038
+ * can be told. A workspace that is not a repository root is one where
2039
+ * `reset --hard` would reach further than the person named.
2040
+ */
2041
+ const misplaced = (0, trusted_git_1.repoRootTroubleAt)(workspace);
2042
+ if (misplaced !== null) {
2043
+ process.stderr.write(`run: ${misplaced}\n`);
2044
+ return 1;
2045
+ }
2046
+ }
2047
+ /**
2048
+ * MAKE ONE, RATHER THAN BE HANDED ONE.
2049
+ *
2050
+ * 🔴 THE OTHER HALF OF THE LIFECYCLE WAS ALSO UNREACHABLE. `looply runs
2051
+ * --reap` only ever removes a checkout under this project's workspaces root —
2052
+ * that containment gate is what stops a reaper deleting a directory a person
2053
+ * made — and nothing in production ever put a checkout there.
2054
+ * `createWorkspace` had no caller, so `--reap` refused every orphan it was
2055
+ * ever shown, and the phase's own DEMO J passed by never reaching the code it
2056
+ * named.
2057
+ *
2058
+ * `--new-workspace` is the way in. Looply cuts the checkout, so looply may
2059
+ * later remove it; a `--in` path a person chose stays theirs, and `--reap`
2060
+ * still refuses it. That asymmetry is the point, not an oversight.
2061
+ *
2062
+ * ⚠️ MUTUALLY EXCLUSIVE WITH `--in`, because they answer the same question and
2063
+ * somebody who typed both meant one of them.
2064
+ */
2065
+ if (argv.includes('--new-workspace')) {
2066
+ if (workspace !== undefined) {
2067
+ process.stderr.write('run: --in and --new-workspace both say where to run; pick one\n');
2068
+ return 1;
2069
+ }
2070
+ const here = process.cwd();
2071
+ const notARoot = (0, trusted_git_1.repoRootTroubleAt)(here);
2072
+ if (notARoot !== null) {
2073
+ process.stderr.write(`run: ${notARoot}\n`);
2074
+ return 1;
2075
+ }
2076
+ try {
2077
+ const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(here));
2078
+ const base = await git.headSha();
2079
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
2080
+ const tag = `ws-${stamp}-${Math.floor(Math.random() * 1e6).toString(36)}`;
2081
+ // Its own branch from the outset: a worktree cannot share a checked-out
2082
+ // branch with another, and the round runner cuts its run branch inside.
2083
+ //
2084
+ /**
2085
+ * 🔴 AND IT IS PROVISIONED BEFORE ANYTHING RUNS IN IT. A worktree holds
2086
+ * only what git tracks, the contained gate has no network by design, and
2087
+ * so a second worker's gate could not pass for any project with a
2088
+ * dependency — measured as four rounds red on the same missing toolchain.
2089
+ * `run.carry_into_checkouts` names what must travel; empty means nothing
2090
+ * is copied and this is byte-identical to what it was.
2091
+ *
2092
+ * Read from the config the run itself will read, so the two cannot
2093
+ * disagree about what this checkout needs.
2094
+ */
2095
+ const carry = loadConfig((0, path_1.join)(here, git_driver_1.STATE_DIR, 'config.yaml')).run.carryIntoCheckouts ?? [];
2096
+ workspace = await (0, workspace_1.createWorkspace)(git, here, tag, `looply/${tag}`, base, (0, node_os_1.homedir)(), carry);
2097
+ process.stdout.write(` workspace: ${workspace}\n`);
2098
+ /**
2099
+ * ⚠️ AND IT SAYS WHAT IT CANNOT DO. The queue is EXECUTION-scoped — one
2100
+ * whole-file rewrite per checkout, which is what makes it safe without a
2101
+ * lock — so a freshly cut workspace starts with an empty one, and a run
2102
+ * there has nothing to pick.
2103
+ *
2104
+ * That is not an oversight: getting work INTO a second execution is a
2105
+ * coordinator's job, and Phase 1 is deliberately not one. What WOULD be an
2106
+ * oversight is a command that cuts a checkout, says nothing, and leaves a
2107
+ * person watching a run end with "nothing to do" and no idea why.
2108
+ */
2109
+ process.stdout.write(` ${(0, ui_1.dim)('note: a new workspace starts with an empty queue — nothing assigns work to it yet.')}\n`);
2110
+ process.stdout.write(` ${(0, ui_1.dim)('`looply run --in <path>` runs in a checkout that already has one.')}\n`);
2111
+ }
2112
+ catch (e) {
2113
+ process.stderr.write(`run: could not make a workspace — ${e.message}\n`);
2114
+ return 1;
2115
+ }
2116
+ }
2117
+ /**
2118
+ * 🔴 THE CHEAPEST REFUSAL, ASKED BEFORE THE TWO MOST EXPENSIVE STEPS.
2119
+ *
2120
+ * The runner refuses a dirty tree as `run()`'s first statement — §12 case 1,
2121
+ * because uncommitted work would be folded into round 1's diff and the first
2122
+ * discard would destroy it. That refusal is right and stays where it is.
2123
+ *
2124
+ * ⚠️ BUT `run()` IS CALLED AFTER COMPILE AND VERIFY, WHICH ARE MODEL CALLS.
2125
+ * Measured: a run against a tree with ONE changed path compiled the goal,
2126
+ * verified both items, and only then refused — **five provider calls across
2127
+ * two runs that bought nothing**. The question costs a `git status`.
2128
+ *
2129
+ * ⚠️ IT ASKS ABOUT THE TREE THE RUN WILL ACTUALLY USE, which is the property
2130
+ * that makes it safe: the runner asks the same question of the same checkout,
2131
+ * so this can only ever change WHEN the refusal happens, never WHETHER.
2132
+ *
2133
+ * 🔴 AND IT SKIPS `--dry-run`, WHICH NEVER REACHES THE RUNNER. A dry run
2134
+ * returns before any round begins and is precisely when somebody wants to ask
2135
+ * "what would happen" without committing their edits first; refusing it here
2136
+ * would be a new refusal, not an earlier one.
2137
+ *
2138
+ * ⚠️ An unreadable checkout is NOT refused here. That is the runner's own
2139
+ * question to answer with its own driver, and guessing at it from outside
2140
+ * would refuse runs this has no business judging.
2141
+ */
2142
+ if (!argv.includes('--dry-run')) {
2143
+ const where = workspace ?? process.cwd();
2144
+ let dirty = null;
2145
+ try {
2146
+ dirty = await new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(where)).changedPaths();
2147
+ }
2148
+ catch {
2149
+ dirty = null;
2150
+ }
2151
+ if (dirty !== null && dirty.length > 0) {
2152
+ process.stderr.write(`run: working tree has ${dirty.length} changed path(s) — refusing to start (commit or stash first)\n`);
2153
+ return 1;
2154
+ }
2155
+ }
2156
+ const at = argv.indexOf('--config');
2157
+ const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
2158
+ if (!configPath) {
2159
+ process.stderr.write('run: --config needs a path\n');
2160
+ return 1;
2161
+ }
2162
+ /**
2163
+ * 🔴 A DECLARED CROSS-REPO NEED GATES THE RUN, and it is asked before the
2164
+ * compile for the same reason the dirty tree is: the answer is local and free,
2165
+ * and finding it out after two model calls is money for nothing.
2166
+ *
2167
+ * ⚠️ A DRY RUN IS TOLD, NOT REFUSED. It exists to answer "what would happen",
2168
+ * and what would happen is precisely that this need blocks — so saying so is
2169
+ * the useful answer and refusing would withhold it.
2170
+ */
2171
+ const needsTrouble = await crossRepoNeedsTrouble(configPath);
2172
+ if (needsTrouble !== null) {
2173
+ if (argv.includes('--dry-run')) {
2174
+ process.stdout.write(`dry run: this project would not start — ${needsTrouble}\n`);
2175
+ }
2176
+ else {
2177
+ process.stderr.write(`run: not starting — ${needsTrouble}\n`);
2178
+ process.stderr.write(' `looply repos` shows where each declared repository stands.\n');
2179
+ return 1;
2180
+ }
2181
+ }
2182
+ const hoursAt = argv.indexOf('--hours');
2183
+ let hours;
2184
+ if (hoursAt >= 0) {
2185
+ const raw = Number(argv[hoursAt + 1]);
2186
+ if (!Number.isFinite(raw) || raw <= 0) {
2187
+ process.stderr.write('run: --hours needs a positive number\n');
2188
+ return 1;
2189
+ }
2190
+ hours = raw;
2191
+ }
2192
+ // Passed by whatever asked for this run, so the lease can record which
2193
+ // request it is carrying out. Absent for a hand-typed run, and REFUSED when
2194
+ // present but empty.
2195
+ //
2196
+ // The flag keeps the name it has always had while the value generalises: a
2197
+ // command id when a person tapped Start, `autonomyStartId(C, N)` when a
2198
+ // controller spent an ordinal. Nothing below this line parses it.
2199
+ //
2200
+ // Empty is refused rather than dropped because dropping it produces a run
2201
+ // that is attributed to NOTHING — no lease field, no start mark, no refusal
2202
+ // mark — which is invisible to whatever asked for it, for ever. That is a
2203
+ // worse outcome than not running, and it is a shape only a caller composing
2204
+ // its own identity can reach.
2205
+ const cmdAt = argv.indexOf('--command-id');
2206
+ const commandId = cmdAt >= 0 ? argv[cmdAt + 1] : undefined;
2207
+ if (cmdAt >= 0 && (commandId === undefined || commandId === '')) {
2208
+ process.stderr.write('run: --command-id needs a request identity\n');
2209
+ return 1;
2210
+ }
2211
+ // WHICH TRY THIS IS, when something is counting them.
2212
+ //
2213
+ // Present only for an autonomous Start. A human Start is bounded by the
2214
+ // server's redelivery window and needs no attempt of its own; giving it one
2215
+ // would put a second, smaller cap on a path that has worked for phases and
2216
+ // change behaviour nobody asked to change.
2217
+ const attemptAt = argv.indexOf('--request-attempt');
2218
+ let requestAttempt;
2219
+ if (attemptAt >= 0) {
2220
+ const raw = Number(argv[attemptAt + 1]);
2221
+ if (!Number.isInteger(raw) || raw < 1 || raw > start_attempts_1.MAX_START_REQUEST_ATTEMPTS) {
2222
+ process.stderr.write(`run: --request-attempt needs a whole number from 1 to ${start_attempts_1.MAX_START_REQUEST_ATTEMPTS}\n`);
2223
+ return 1;
2224
+ }
2225
+ if (commandId === undefined) {
2226
+ // An attempt number counts tries at a REQUEST. Without one there is
2227
+ // nothing to count, and nothing to file the claim under.
2228
+ process.stderr.write('run: --request-attempt needs a --command-id\n');
2229
+ return 1;
2230
+ }
2231
+ requestAttempt = raw;
2232
+ }
2233
+ // ───────────────────────────────────────────────────────────────────────────
2234
+ // `--autonomous` IS TRANSPORT, NOT PERMISSION.
2235
+ //
2236
+ // Its only effect is that beginning this run does not consume a person's
2237
+ // pending pause or stop. That is a real power — it lets a run proceed past an
2238
+ // instruction a person left — so a flag on a command line must not be enough
2239
+ // to claim it. Anyone who can type `looply run` could otherwise type
2240
+ // `--autonomous` and walk through somebody's Stop.
2241
+ //
2242
+ // So the flag only ANNOUNCES what the child must independently establish: an
2243
+ // attempt number in range, a request to file it under, and — below, at the
2244
+ // admission seam — an exclusive claim that actually succeeded. The claim is
2245
+ // the proof, because only the autonomy path produces one and the filesystem
2246
+ // decides who gets it.
2247
+ //
2248
+ // Announced without the rest, this is a misuse rather than a lesser request,
2249
+ // and it is refused outright instead of being quietly downgraded. Nothing
2250
+ // legitimate sends one without the other: the launcher emits both together.
2251
+ const announcedAutonomous = argv.includes('--autonomous');
2252
+ if (announcedAutonomous && requestAttempt === undefined) {
2253
+ process.stderr.write('run: --autonomous needs the --request-attempt it belongs to\n');
2254
+ return 1;
2255
+ }
2256
+ // The goal binding, carried across from the daemon. All three or none: a
2257
+ // goal with no version cannot be checked for freshness, and item ids with no
2258
+ // goal name work for nobody. Parsed strictly here rather than half-applied
2259
+ // later, because a run that starts UNBOUND when a goal was asked for works
2260
+ // the whole queue while the phone waits for a goal that never binds.
2261
+ const goalAt = argv.indexOf('--goal-id');
2262
+ const goalId = goalAt >= 0 ? argv[goalAt + 1] : undefined;
2263
+ const versionAt = argv.indexOf('--goal-version');
2264
+ const rawVersion = versionAt >= 0 ? argv[versionAt + 1] : undefined;
2265
+ const itemsAt = argv.indexOf('--item-ids');
2266
+ const rawItems = itemsAt >= 0 ? argv[itemsAt + 1] : undefined;
2267
+ let goalVersion;
2268
+ if (goalId !== undefined) {
2269
+ if (rawVersion === undefined) {
2270
+ process.stderr.write('run: --goal-id needs a --goal-version\n');
2271
+ return 1;
2272
+ }
2273
+ goalVersion = Number(rawVersion);
2274
+ if (!Number.isInteger(goalVersion) || goalVersion < 0) {
2275
+ process.stderr.write('run: --goal-version needs a whole number\n');
2276
+ return 1;
2277
+ }
2278
+ }
2279
+ else if (rawVersion !== undefined || rawItems !== undefined) {
2280
+ process.stderr.write('run: --goal-version and --item-ids need a --goal-id\n');
2281
+ return 1;
2282
+ }
2283
+ // The answer this run is picking up, if a person started again after one.
2284
+ // Carried as an id, resolved against the server inside the run — the daemon
2285
+ // does not tell a run what a decision said, only which decision to go and
2286
+ // read. It needs a goal for the same reason the version does: the resolution
2287
+ // is checked against the pursuit this run is actually bound to.
2288
+ const resumeAt = argv.indexOf('--resumed-from-approval');
2289
+ const resumedFromApprovalId = resumeAt >= 0 ? argv[resumeAt + 1] : undefined;
2290
+ if (resumedFromApprovalId !== undefined) {
2291
+ if (resumedFromApprovalId.length === 0 || resumedFromApprovalId.startsWith('--')) {
2292
+ process.stderr.write('run: --resumed-from-approval needs an approval id\n');
2293
+ return 1;
2294
+ }
2295
+ if (goalId === undefined) {
2296
+ process.stderr.write('run: --resumed-from-approval needs a --goal-id\n');
2297
+ return 1;
2298
+ }
2299
+ }
2300
+ /**
2301
+ * The approved action this Start was accepted to carry out.
2302
+ *
2303
+ * PARSED HERE AND PROVEN NOWHERE HERE. An id on a command line is something
2304
+ * anybody can type, and this flag buys exactly nothing on its own: the run
2305
+ * refuses it unless a durable reservation, written by the Start gate before
2306
+ * this process existed, names this run's own `--command-id`. That check lives
2307
+ * in `governed-run`, where the rest of the governed vocabulary lives.
2308
+ *
2309
+ * The goal requirement is the same one the resume flag has, for the same
2310
+ * reason: an approval belongs to a pursuit at a version, and a run that names
2311
+ * none has nothing to check it against.
2312
+ */
2313
+ const governedAt = argv.indexOf('--governed-approval');
2314
+ const governedApprovalId = governedAt >= 0 ? argv[governedAt + 1] : undefined;
2315
+ if (governedApprovalId !== undefined) {
2316
+ if (governedApprovalId.length === 0 || governedApprovalId.startsWith('--')) {
2317
+ process.stderr.write('run: --governed-approval needs an approval id\n');
2318
+ return 1;
2319
+ }
2320
+ if (goalId === undefined) {
2321
+ process.stderr.write('run: --governed-approval needs a --goal-id\n');
2322
+ return 1;
2323
+ }
2324
+ if (resumedFromApprovalId !== undefined) {
2325
+ // A run picks up an answer or carries one out. Both at once would be a
2326
+ // run acting on a refusal and an approval in the same breath.
2327
+ process.stderr.write('run: a run cannot both resume a decision and carry one out\n');
2328
+ return 1;
2329
+ }
2330
+ }
2331
+ // An empty value is not an empty id: `--item-ids ''` means "adopt nothing
2332
+ // new", which is what a second run of the same goal asks for. Splitting it
2333
+ // naively would produce one item whose id is the empty string.
2334
+ const itemIds = rawItems === undefined ? undefined : rawItems === '' ? [] : rawItems.split(',');
2335
+ if (itemIds?.some((i) => i.length === 0)) {
2336
+ process.stderr.write('run: --item-ids has an empty id\n');
2337
+ return 1;
2338
+ }
2339
+ // ─────────────────────────────────────────────────────────────────────────
2340
+ // THE ADMISSION SEAM.
2341
+ //
2342
+ // Before the config is read, before the goal is resolved, before the lease is
2343
+ // taken, before a single agent call — this process states that it is the one
2344
+ // executing this attempt, and the filesystem decides whether it is.
2345
+ //
2346
+ // It is HERE, in the child, rather than in the daemon that spawned it,
2347
+ // because children are detached and outlive their parents. A claim written by
2348
+ // the daemon would record the identity of a process whose death proves
2349
+ // nothing about whether the run is still going; a claim written here records
2350
+ // the identity of the only process that can go on to do the work.
2351
+ //
2352
+ // The loser exits by its own door. It has decided nothing about the goal, so
2353
+ // it writes no refusal mark and no start mark — there is nothing to say.
2354
+ let autonomous = false;
2355
+ if (requestAttempt !== undefined && commandId !== undefined) {
2356
+ /**
2357
+ * 🔴 THE AUTHORITY TRAIL IS THE DAEMON'S AND THE PROJECT'S, NOT THIS
2358
+ * CHECKOUT'S — AND READING IT FROM THE WORKSPACE REFUSED EVERY PLACED RUN.
2359
+ *
2360
+ * This was `join(workspace ?? process.cwd(), STATE_DIR, 'state')`, which is
2361
+ * EXECUTION-scoped. Every file it looks for is written by the DAEMON: the
2362
+ * adopted controller, the validated ordinal reservation under it, and the
2363
+ * launch receipt for this attempt. The daemon writes them in its own
2364
+ * checkout; a run the coordinator placed in a workspace looked in the
2365
+ * workspace, found an empty directory, and exited with
2366
+ *
2367
+ * run: no grant on this machine reserved a Start by that name
2368
+ *
2369
+ * MEASURED end to end, and reachable no other way: every unit test on this
2370
+ * path runs in a single checkout, where the project and execution state
2371
+ * directories are THE SAME PATH, so the distinction is invisible. It only
2372
+ * appears the moment a run is placed somewhere else — which is the entire
2373
+ * point of a coordinator.
2374
+ *
2375
+ * `projectStateDir` resolves the repository's identity, so it answers the
2376
+ * same path from the main checkout and from any linked worktree of it. In
2377
+ * an ordinary single-checkout project that path is exactly what this line
2378
+ * produced before, so nothing about Simple Mode changes.
2379
+ */
2380
+ const stateDir = (0, git_driver_1.projectStateDir)(workspace ?? process.cwd());
2381
+ // ─────────────────────────────────────────────────────────────────────
2382
+ // AUTHORITY BEFORE EXCLUSIVITY.
2383
+ //
2384
+ // The exclusive claim below proves UNIQUENESS: one process owns this
2385
+ // attempt. It was being read as permission, and it is not. A certification
2386
+ // pass showed the gap in one line — invent a request id, claim attempt 1
2387
+ // of it uncontested, and the autonomous policy was granted, with no grant,
2388
+ // no controller and no ordinal anywhere in existence.
2389
+ //
2390
+ // So this asks first, and asks about durable facts the DAEMON wrote: an
2391
+ // adopted controller, a validated ordinal reservation under it, a request
2392
+ // id that pair derives, a launch receipt for this attempt, and a spawn
2393
+ // record that — if it has been written yet — describes this process.
2394
+ //
2395
+ // The child asks the server nothing. It does not need to: the daemon
2396
+ // reconciled with the server before it reserved anything, and every step
2397
+ // after that left a file. This is that trail, read back.
2398
+ const authority = await (0, autonomy_authority_1.checkAutonomyAuthority)({ stateDir, fs: start_attempts_1.nodeAttemptFs, pid: process.pid, host: (0, node_os_1.hostname)() }, commandId, requestAttempt);
2399
+ if (!authority.ok) {
2400
+ // NOT a refusal about the goal. Nothing here has looked at a goal, a
2401
+ // queue or a repository — this process simply was not asked for. It
2402
+ // leaves by the non-semantic door, so nothing downstream may conclude
2403
+ // anything about the work.
2404
+ process.stderr.write(`run: ${authority.because}\n`);
2405
+ return run_cmd_1.EXIT_ATTEMPT_CLAIMED;
2406
+ }
2407
+ const attempts = new start_attempts_1.StartAttempts(stateDir, start_attempts_1.nodeAttemptFs);
2408
+ const claimed = await attempts.claim(commandId, requestAttempt);
2409
+ if (!claimed.ok) {
2410
+ process.stderr.write(`run: ${claimed.because}\n`);
2411
+ return run_cmd_1.EXIT_ATTEMPT_CLAIMED;
2412
+ }
2413
+ // EARNED TWICE OVER. The daemon authorized this exact execution, and this
2414
+ // process is the one that got in.
2415
+ autonomous = announcedAutonomous;
2416
+ }
2417
+ /**
2418
+ * RESOLVED INSIDE THE WORKSPACE when there is one. The default
2419
+ * `.looply/config.yaml` is relative, and a run in another checkout must read
2420
+ * THAT checkout's config — otherwise two workspaces would run whichever gates
2421
+ * belonged to the directory the operator happened to be standing in. An
2422
+ * explicit absolute `--config` still wins, because `resolve` leaves it alone.
2423
+ */
2424
+ const cfg = loadConfig(workspace === undefined ? configPath : (0, path_1.resolve)(workspace, configPath));
2425
+ for (const w of (0, config_1.configWarnings)(cfg))
2426
+ process.stdout.write(`warning : ${w}\n`);
2427
+ const log = (line) => {
2428
+ process.stdout.write(`${line}\n`);
2429
+ };
2430
+ let summary;
2431
+ try {
2432
+ summary = await (0, run_cmd_1.runLoop)({
2433
+ cwd: workspace ?? process.cwd(),
2434
+ config: cfg,
2435
+ /**
2436
+ * ⚠️ ASKED PER PICK, NOT ONCE. A sibling publishes while this run is
2437
+ * working, and an item blocked at round 3 should be pickable at round 9 —
2438
+ * a context captured at startup would keep it waiting all night.
2439
+ */
2440
+ crossRepo: () => crossRepoContext(workspace ?? process.cwd(), cfg),
2441
+ ...(hours !== undefined ? { hours } : {}),
2442
+ // `!== undefined`, like every field beside it. Under a truthiness test an
2443
+ // EMPTY `--command-id` was silently dropped, and a run with no request
2444
+ // identity writes no lease field, no start mark and no refusal mark — it
2445
+ // is invisible to whatever asked for it, for ever. Unreachable for a
2446
+ // server command, which always has an id; reachable the moment a
2447
+ // controller composes one.
2448
+ ...(commandId !== undefined ? { commandId } : {}),
2449
+ ...(autonomous ? { autonomous: true } : {}),
2450
+ ...(goalId === undefined ? {} : { goalId }),
2451
+ ...(goalVersion === undefined ? {} : { goalVersion }),
2452
+ ...(itemIds === undefined ? {} : { itemIds }),
2453
+ ...(resumedFromApprovalId === undefined ? {} : { resumedFromApprovalId }),
2454
+ ...(governedApprovalId === undefined ? {} : { governedApprovalId }),
2455
+ dryRun: argv.includes('--dry-run'),
2456
+ /**
2457
+ * 🔴 THE ONLY WAY A PERSON AT A TERMINAL CAN CLEAR A SAFETY HOLD, and
2458
+ * until now there was none. A hold has no expiry by design; the one
2459
+ * retry it carries was spendable only by a paired phone's Start. A
2460
+ * `looply run --goal-id G --goal-version 1` that hit a hold was told to
2461
+ * "start it again", and starting it again hit the same fence for ever.
2462
+ *
2463
+ * ⚠️ IT SPENDS, IT DOES NOT DISABLE. Same exclusive create the daemon
2464
+ * uses, so the second invocation is refused rather than granted.
2465
+ */
2466
+ tryOnceMore: argv.includes('--try-once-more'),
2467
+ log,
2468
+ });
2469
+ }
2470
+ catch (e) {
2471
+ // A refusal retrying cannot fix leaves by its own door. The daemon that
2472
+ // spawned this reads the exit code and stops offering the command, rather
2473
+ // than handing it back every few seconds for the rest of the redelivery
2474
+ // window — which is noise, not resilience. Everything else keeps exiting 1
2475
+ // and stays retryable.
2476
+ if (!(e instanceof run_cmd_1.RunRefused))
2477
+ throw e;
2478
+ // Two audiences, one decision. A person watching a terminal reads this;
2479
+ // the daemon that spawned this cannot, because it spawns with
2480
+ // `stdio: 'ignore'` on a detached child, so the words go nowhere. Both are
2481
+ // told, by the channel each can actually hear.
2482
+ process.stderr.write(`${e.message}
2483
+ `);
2484
+ // Only when something asked. A hand-typed `looply run` has no request to
2485
+ // answer, and writing a mark nobody will ever look up would make this file
2486
+ // a log of every refusal rather than an index of the outstanding one.
2487
+ //
2488
+ // `!== undefined`, matching the forwarding guard above: the two must agree
2489
+ // about what counts as a request, or a run could be attributed on the lease
2490
+ // and unattributed in its refusal. An empty id never reaches here anyway —
2491
+ // it is refused at the parse — and this says so rather than relying on it.
2492
+ //
2493
+ // ONLY the RunRefused message. Not the stack, not anything else this
2494
+ // process printed — `Command.reason` is read by a client, and a refusal is
2495
+ // a decision the machine chose to report, not whatever happened to be on
2496
+ // its way out.
2497
+ if (commandId !== undefined) {
2498
+ try {
2499
+ // The SAME expression the run process uses, not a relative one that
2500
+ // happens to agree: a mark written anywhere else is a mark nobody reads.
2501
+ //
2502
+ // 🔴 IT SAID `process.cwd()` AND MEANT IT, and `--in` made that false.
2503
+ // `runLoop` is given `cwd: workspace ?? process.cwd()` and the admission
2504
+ // seam claims its attempt under the same expression, so a run executing
2505
+ // in a workspace wrote its StartAttempts claim in one directory and its
2506
+ // refusal in another. The phone would then wait out the full retry
2507
+ // window for a reason that was already on disk, one directory over.
2508
+ //
2509
+ // ⚠️ RESIDUAL, STATED RATHER THAN PAPERED OVER: the DAEMON reads marks
2510
+ // from its own state directory. That still meets this one, because
2511
+ // nothing passes `--in` to a daemon-launched run — `--command-id` and
2512
+ // `--in` never appear together today. A later phase that launches into
2513
+ // workspaces must move this channel to project scope AND make it
2514
+ // append-only first: it is a whole-document rewrite, so sharing it
2515
+ // between concurrent runs without that is the lost-update class.
2516
+ /**
2517
+ * 🔴 PROJECT-SCOPED, AS THE COMMENT ABOVE PREDICTED IT WOULD HAVE TO
2518
+ * BE. "A later phase that launches into workspaces must move this
2519
+ * channel to project scope AND make it append-only first" — this is
2520
+ * that phase, and `RunRefusalMarks` now appends beside its index for
2521
+ * exactly the lost-update reason named there.
2522
+ *
2523
+ * The daemon reads this from the PROJECT's state, so a placed run
2524
+ * writing it beside its workspace left every refusal unreadable: the
2525
+ * launcher fell back to "the run exited — exit 3" with no code, and
2526
+ * nothing downstream could conclude anything about the goal.
2527
+ */
2528
+ const marks = new run_refusal_marks_1.RunRefusalMarks((0, git_driver_1.projectStateDir)(workspace ?? process.cwd()), ledger_store_1.nodeLedgerFs);
2529
+ await marks.load();
2530
+ // The CODE as well as the words. `e` is a `RunRefused` — the line above
2531
+ // rethrows anything else — so its code is in scope and typed here, and
2532
+ // until 6D2A it was simply dropped on the floor at this one line.
2533
+ await marks.record({
2534
+ commandId,
2535
+ reason: e.message,
2536
+ code: e.code,
2537
+ // M5-2: and the branch, when the run had one — the lane's key to the grant.
2538
+ ...(e.branch === undefined ? {} : { branch: e.branch }),
2539
+ // L4b: and what key 2 confirmed, on a met goal -- the facts the settle carries.
2540
+ ...(e.delivery === undefined ? {} : { delivery: e.delivery }),
2541
+ at: new Date().toISOString(),
2542
+ });
2543
+ }
2544
+ catch {
2545
+ // Never fatal, and never changes the answer. Failing to write the
2546
+ // reason costs the phone a sentence; failing to exit 3 would cost it
2547
+ // the refusal itself, and be retried every three seconds for an hour.
2548
+ }
2549
+ }
2550
+ return run_cmd_1.EXIT_REFUSED;
2551
+ }
2552
+ const { rounds, committed, discarded, abandoned } = summary.totals;
2553
+ log('');
2554
+ log(`stopped : ${summary.stopReason}`);
2555
+ log(`rounds : ${rounds} — ${committed} kept, ${discarded} discarded, ${abandoned} abandoned`);
2556
+ // A run that never discarded anything is not a triumph, it is a signal: the
2557
+ // prover is the only thing that can fail a round on quality, and one that
2558
+ // never fires is one that is not working.
2559
+ if (rounds >= 10 && discarded === 0 && cfg.proof.enabled) {
2560
+ log('');
2561
+ log('note : nothing was discarded across the whole run. A prover that never');
2562
+ log(' rejects is usually misconfigured rather than lucky — check it.');
2563
+ }
2564
+ return 0;
2565
+ }
2566
+ /**
2567
+ * Read enough of the world to render the welcome or the agent snapshot.
2568
+ *
2569
+ * Every read is optional. A folder with no looply in it must still get a useful
2570
+ * screen, so a missing file is a fact about the state rather than an error.
2571
+ */
2572
+ function readWorld() {
2573
+ let config = null;
2574
+ try {
2575
+ config = (0, config_1.parseConfig)((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/config.yaml'), 'utf8'));
2576
+ }
2577
+ catch {
2578
+ config = null;
2579
+ }
2580
+ let status = null;
2581
+ try {
2582
+ const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8'));
2583
+ status = parsed;
2584
+ }
2585
+ catch {
2586
+ status = null;
2587
+ }
2588
+ let items = [];
2589
+ try {
2590
+ const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/queue.json'), 'utf8'));
2591
+ if (Array.isArray(parsed))
2592
+ items = parsed;
2593
+ else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) {
2594
+ items = parsed.items;
2595
+ }
2596
+ }
2597
+ catch {
2598
+ items = [];
2599
+ }
2600
+ let machine = { paired: false, id: null, serverUrl: null };
2601
+ try {
2602
+ const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
2603
+ if (identity)
2604
+ machine = { paired: true, id: identity.machineId, serverUrl: identity.serverUrl };
2605
+ }
2606
+ catch {
2607
+ // A corrupt identity file is reported by `pair` and `daemon`, which can act
2608
+ // on it. Here it just means "not paired" for display purposes.
2609
+ }
2610
+ return { config, status, items, machine };
2611
+ }
2612
+ /** The bare `looply` screen: where you are, what you can do, what to do next. */
2613
+ function welcome() {
2614
+ const world = readWorld();
2615
+ const status = world.status;
2616
+ process.stdout.write((0, welcome_1.renderWelcome)({
2617
+ paired: world.machine.paired,
2618
+ ...(world.machine.id ? { machineName: world.machine.id } : {}),
2619
+ ...(world.machine.serverUrl ? { serverUrl: world.machine.serverUrl } : {}),
2620
+ configured: world.config !== null,
2621
+ ...(world.config ? { projectName: world.config.project.name } : {}),
2622
+ readyItems: world.items.filter((i) => i.state === 'ready').length,
2623
+ draftItems: world.items.filter((i) => i.state === 'draft' || i.state === 'verifying').length,
2624
+ runLive: status?.live === true,
2625
+ currentItem: status?.round?.title ?? null,
2626
+ }, daemon_cmd_1.CLI_VERSION));
2627
+ return 0;
2628
+ }
2629
+ /**
2630
+ * `looply agent` — state for another program.
2631
+ *
2632
+ * Exits 0 whenever it could answer, including "nothing is running". A non-zero
2633
+ * exit means the question could not be answered, never that the answer was dull.
2634
+ */
2635
+ function agent(argv) {
2636
+ const world = readWorld();
2637
+ const snapshot = (0, agent_cmd_1.buildSnapshot)({
2638
+ cliVersion: daemon_cmd_1.CLI_VERSION,
2639
+ config: world.config,
2640
+ status: world.status,
2641
+ items: world.items,
2642
+ machine: world.machine,
2643
+ now: Date.now(),
2644
+ });
2645
+ const json = argv.includes('--json') || !argv.includes('--text');
2646
+ process.stdout.write(json ? (0, agent_cmd_1.renderAgentJson)(snapshot) : (0, agent_cmd_1.renderAgentText)(snapshot));
2647
+ return 0;
2648
+ }
2649
+ async function main(argv) {
2650
+ // Honoured before anything prints, so a piped or redirected run is clean.
2651
+ if (argv.includes('--no-color'))
2652
+ (0, ui_1.setColor)(false);
2653
+ const [cmd, ...rest] = argv.filter((a) => a !== '--no-color');
2654
+ switch (cmd) {
2655
+ case 'init':
2656
+ return init(rest);
2657
+ case 'doctor':
2658
+ return doctor(rest);
2659
+ case 'pair':
2660
+ return pair(rest);
2661
+ case 'daemon':
2662
+ return daemon(rest);
2663
+ case 'serve':
2664
+ return (0, serve_cmd_1.serveCommand)(rest, (0, serve_cmd_1.nodeServeDeps)());
2665
+ case 'scan':
2666
+ return scan(rest);
2667
+ case 'verify':
2668
+ return verify(rest);
2669
+ case 'queue':
2670
+ return queue(rest);
2671
+ case 'integrate':
2672
+ return integrate(rest);
2673
+ case 'runs':
2674
+ return runs(rest);
2675
+ case 'source':
2676
+ return (0, source_cmd_1.sourceCommand)(rest, (0, source_cmd_1.nodeSourceDeps)());
2677
+ case 'stop':
2678
+ return stop(rest);
2679
+ case 'pause':
2680
+ return pauseOrResume('pause', rest);
2681
+ case 'resume':
2682
+ return pauseOrResume('resume', rest);
2683
+ case 'run':
2684
+ return run(rest);
2685
+ case 'status':
2686
+ return status();
2687
+ case 'report':
2688
+ return report();
2689
+ case 'goal':
2690
+ return goal(rest);
2691
+ case 'goals':
2692
+ return goals();
2693
+ case 'repos':
2694
+ return (0, repos_cmd_1.reposCommand)(rest, nodeReposDeps());
2695
+ case 'sessions':
2696
+ return (0, sessions_cmd_1.sessionsCommand)(rest, nodeSessionsDeps());
2697
+ case 'evidence':
2698
+ return (0, evidence_cmd_1.evidenceCommand)(rest, nodeEvidenceDeps());
2699
+ case 'version':
2700
+ case '--version':
2701
+ case '-v':
2702
+ process.stdout.write('0.1.0\n');
2703
+ return 0;
2704
+ case undefined:
2705
+ return welcome();
2706
+ case 'agent':
2707
+ return agent(rest);
2708
+ case 'help':
2709
+ case '--help':
2710
+ case '-h':
2711
+ process.stdout.write(USAGE);
2712
+ return 0;
2713
+ default:
2714
+ process.stderr.write(`looply: unknown command ${JSON.stringify(cmd)}\n\n${USAGE}`);
2715
+ return 64;
2716
+ }
2717
+ }
2718
+ /* istanbul ignore next — process wiring, exercised by running the binary */
2719
+ if (require.main === module) {
2720
+ main(process.argv.slice(2)).then((code) => {
2721
+ process.exitCode = code;
2722
+ }, (e) => {
2723
+ process.stderr.write(`${e.message}\n`);
2724
+ process.exitCode = 1;
2725
+ });
2726
+ }
2727
+ //# sourceMappingURL=bin.js.map