looply-cli 0.0.0-stage → 0.2.0

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