looply-cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (785) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -3
  3. package/dist/.src-hash +1 -0
  4. package/dist/acceptance-observe.d.ts +108 -0
  5. package/dist/acceptance-observe.d.ts.map +1 -0
  6. package/dist/acceptance-observe.js +152 -0
  7. package/dist/acceptance-observe.js.map +1 -0
  8. package/dist/agent-cmd.d.ts +131 -0
  9. package/dist/agent-cmd.d.ts.map +1 -0
  10. package/dist/agent-cmd.js +154 -0
  11. package/dist/agent-cmd.js.map +1 -0
  12. package/dist/agent-contained.d.ts +114 -0
  13. package/dist/agent-contained.d.ts.map +1 -0
  14. package/dist/agent-contained.js +292 -0
  15. package/dist/agent-contained.js.map +1 -0
  16. package/dist/agent-driver.d.ts +130 -0
  17. package/dist/agent-driver.d.ts.map +1 -0
  18. package/dist/agent-driver.js +170 -0
  19. package/dist/agent-driver.js.map +1 -0
  20. package/dist/agent-envelope.d.ts +94 -0
  21. package/dist/agent-envelope.d.ts.map +1 -0
  22. package/dist/agent-envelope.js +214 -0
  23. package/dist/agent-envelope.js.map +1 -0
  24. package/dist/agent-exec.d.ts +182 -0
  25. package/dist/agent-exec.d.ts.map +1 -0
  26. package/dist/agent-exec.js +125 -0
  27. package/dist/agent-exec.js.map +1 -0
  28. package/dist/agent-runtime.d.ts +23 -0
  29. package/dist/agent-runtime.d.ts.map +1 -0
  30. package/dist/agent-runtime.js +143 -0
  31. package/dist/agent-runtime.js.map +1 -0
  32. package/dist/approval-gate-read.d.ts +66 -0
  33. package/dist/approval-gate-read.d.ts.map +1 -0
  34. package/dist/approval-gate-read.js +138 -0
  35. package/dist/approval-gate-read.js.map +1 -0
  36. package/dist/approval-resolution.d.ts +87 -0
  37. package/dist/approval-resolution.d.ts.map +1 -0
  38. package/dist/approval-resolution.js +100 -0
  39. package/dist/approval-resolution.js.map +1 -0
  40. package/dist/approval-resumes.d.ts +102 -0
  41. package/dist/approval-resumes.d.ts.map +1 -0
  42. package/dist/approval-resumes.js +167 -0
  43. package/dist/approval-resumes.js.map +1 -0
  44. package/dist/ask-brain.d.ts +167 -0
  45. package/dist/ask-brain.d.ts.map +1 -0
  46. package/dist/ask-brain.js +355 -0
  47. package/dist/ask-brain.js.map +1 -0
  48. package/dist/autonomy-authority.d.ts +80 -0
  49. package/dist/autonomy-authority.d.ts.map +1 -0
  50. package/dist/autonomy-authority.js +82 -0
  51. package/dist/autonomy-authority.js.map +1 -0
  52. package/dist/autonomy-controller.d.ts +321 -0
  53. package/dist/autonomy-controller.d.ts.map +1 -0
  54. package/dist/autonomy-controller.js +517 -0
  55. package/dist/autonomy-controller.js.map +1 -0
  56. package/dist/autonomy-store.d.ts +327 -0
  57. package/dist/autonomy-store.d.ts.map +1 -0
  58. package/dist/autonomy-store.js +581 -0
  59. package/dist/autonomy-store.js.map +1 -0
  60. package/dist/autonomy-tick.d.ts +257 -0
  61. package/dist/autonomy-tick.d.ts.map +1 -0
  62. package/dist/autonomy-tick.js +616 -0
  63. package/dist/autonomy-tick.js.map +1 -0
  64. package/dist/bin.d.ts +3 -0
  65. package/dist/bin.d.ts.map +1 -0
  66. package/dist/bin.js +2719 -0
  67. package/dist/bin.js.map +1 -0
  68. package/dist/brain-advice.d.ts +21 -0
  69. package/dist/brain-advice.d.ts.map +1 -0
  70. package/dist/brain-advice.js +51 -0
  71. package/dist/brain-advice.js.map +1 -0
  72. package/dist/candidate-replay-runtime.d.ts +125 -0
  73. package/dist/candidate-replay-runtime.d.ts.map +1 -0
  74. package/dist/candidate-replay-runtime.js +188 -0
  75. package/dist/candidate-replay-runtime.js.map +1 -0
  76. package/dist/checkout-carry.d.ts +76 -0
  77. package/dist/checkout-carry.d.ts.map +1 -0
  78. package/dist/checkout-carry.js +125 -0
  79. package/dist/checkout-carry.js.map +1 -0
  80. package/dist/citation-readers.d.ts +26 -0
  81. package/dist/citation-readers.d.ts.map +1 -0
  82. package/dist/citation-readers.js +102 -0
  83. package/dist/citation-readers.js.map +1 -0
  84. package/dist/command-receipts.d.ts +79 -0
  85. package/dist/command-receipts.d.ts.map +1 -0
  86. package/dist/command-receipts.js +143 -0
  87. package/dist/command-receipts.js.map +1 -0
  88. package/dist/command-runner.d.ts +32 -0
  89. package/dist/command-runner.d.ts.map +1 -0
  90. package/dist/command-runner.js +108 -0
  91. package/dist/command-runner.js.map +1 -0
  92. package/dist/compatible-sets.d.ts +67 -0
  93. package/dist/compatible-sets.d.ts.map +1 -0
  94. package/dist/compatible-sets.js +81 -0
  95. package/dist/compatible-sets.js.map +1 -0
  96. package/dist/compile-check.d.ts +31 -0
  97. package/dist/compile-check.d.ts.map +1 -0
  98. package/dist/compile-check.js +34 -0
  99. package/dist/compile-check.js.map +1 -0
  100. package/dist/compile-store.d.ts +16 -0
  101. package/dist/compile-store.d.ts.map +1 -0
  102. package/dist/compile-store.js +21 -0
  103. package/dist/compile-store.js.map +1 -0
  104. package/dist/completion.d.ts +106 -0
  105. package/dist/completion.d.ts.map +1 -0
  106. package/dist/completion.js +131 -0
  107. package/dist/completion.js.map +1 -0
  108. package/dist/config.d.ts +549 -0
  109. package/dist/config.d.ts.map +1 -0
  110. package/dist/config.js +763 -0
  111. package/dist/config.js.map +1 -0
  112. package/dist/control-file.d.ts +142 -0
  113. package/dist/control-file.d.ts.map +1 -0
  114. package/dist/control-file.js +144 -0
  115. package/dist/control-file.js.map +1 -0
  116. package/dist/coordinator-state.d.ts +131 -0
  117. package/dist/coordinator-state.d.ts.map +1 -0
  118. package/dist/coordinator-state.js +380 -0
  119. package/dist/coordinator-state.js.map +1 -0
  120. package/dist/coordinator.d.ts +95 -0
  121. package/dist/coordinator.d.ts.map +1 -0
  122. package/dist/coordinator.js +97 -0
  123. package/dist/coordinator.js.map +1 -0
  124. package/dist/cursor-store.d.ts +53 -0
  125. package/dist/cursor-store.d.ts.map +1 -0
  126. package/dist/cursor-store.js +98 -0
  127. package/dist/cursor-store.js.map +1 -0
  128. package/dist/daemon-cmd.d.ts +239 -0
  129. package/dist/daemon-cmd.d.ts.map +1 -0
  130. package/dist/daemon-cmd.js +1751 -0
  131. package/dist/daemon-cmd.js.map +1 -0
  132. package/dist/daemon-lock.d.ts +140 -0
  133. package/dist/daemon-lock.d.ts.map +1 -0
  134. package/dist/daemon-lock.js +352 -0
  135. package/dist/daemon-lock.js.map +1 -0
  136. package/dist/daemon-recovery.d.ts +78 -0
  137. package/dist/daemon-recovery.d.ts.map +1 -0
  138. package/dist/daemon-recovery.js +161 -0
  139. package/dist/daemon-recovery.js.map +1 -0
  140. package/dist/daemon.d.ts +368 -0
  141. package/dist/daemon.d.ts.map +1 -0
  142. package/dist/daemon.js +601 -0
  143. package/dist/daemon.js.map +1 -0
  144. package/dist/declared-response-shape.d.ts +93 -0
  145. package/dist/declared-response-shape.d.ts.map +1 -0
  146. package/dist/declared-response-shape.js +200 -0
  147. package/dist/declared-response-shape.js.map +1 -0
  148. package/dist/declared-surfaces.d.ts +16 -0
  149. package/dist/declared-surfaces.d.ts.map +1 -0
  150. package/dist/declared-surfaces.js +19 -0
  151. package/dist/declared-surfaces.js.map +1 -0
  152. package/dist/depends-review.d.ts +113 -0
  153. package/dist/depends-review.d.ts.map +1 -0
  154. package/dist/depends-review.js +110 -0
  155. package/dist/depends-review.js.map +1 -0
  156. package/dist/doctor.d.ts +91 -0
  157. package/dist/doctor.d.ts.map +1 -0
  158. package/dist/doctor.js +71 -0
  159. package/dist/doctor.js.map +1 -0
  160. package/dist/edge-approval.d.ts +105 -0
  161. package/dist/edge-approval.d.ts.map +1 -0
  162. package/dist/edge-approval.js +100 -0
  163. package/dist/edge-approval.js.map +1 -0
  164. package/dist/evidence-cache.d.ts +45 -0
  165. package/dist/evidence-cache.d.ts.map +1 -0
  166. package/dist/evidence-cache.js +90 -0
  167. package/dist/evidence-cache.js.map +1 -0
  168. package/dist/evidence-cmd.d.ts +133 -0
  169. package/dist/evidence-cmd.d.ts.map +1 -0
  170. package/dist/evidence-cmd.js +347 -0
  171. package/dist/evidence-cmd.js.map +1 -0
  172. package/dist/evidence-facts.d.ts +40 -0
  173. package/dist/evidence-facts.d.ts.map +1 -0
  174. package/dist/evidence-facts.js +114 -0
  175. package/dist/evidence-facts.js.map +1 -0
  176. package/dist/exclusive-lock.d.ts +52 -0
  177. package/dist/exclusive-lock.d.ts.map +1 -0
  178. package/dist/exclusive-lock.js +150 -0
  179. package/dist/exclusive-lock.js.map +1 -0
  180. package/dist/exec-domains.d.ts +140 -0
  181. package/dist/exec-domains.d.ts.map +1 -0
  182. package/dist/exec-domains.js +287 -0
  183. package/dist/exec-domains.js.map +1 -0
  184. package/dist/exemption-store.d.ts +197 -0
  185. package/dist/exemption-store.d.ts.map +1 -0
  186. package/dist/exemption-store.js +266 -0
  187. package/dist/exemption-store.js.map +1 -0
  188. package/dist/explicit-start-gate.d.ts +96 -0
  189. package/dist/explicit-start-gate.d.ts.map +1 -0
  190. package/dist/explicit-start-gate.js +103 -0
  191. package/dist/explicit-start-gate.js.map +1 -0
  192. package/dist/fs-trouble.d.ts +70 -0
  193. package/dist/fs-trouble.d.ts.map +1 -0
  194. package/dist/fs-trouble.js +98 -0
  195. package/dist/fs-trouble.js.map +1 -0
  196. package/dist/gate.d.ts +278 -0
  197. package/dist/gate.d.ts.map +1 -0
  198. package/dist/gate.js +278 -0
  199. package/dist/gate.js.map +1 -0
  200. package/dist/git-driver.d.ts +519 -0
  201. package/dist/git-driver.d.ts.map +1 -0
  202. package/dist/git-driver.js +900 -0
  203. package/dist/git-driver.js.map +1 -0
  204. package/dist/git-push-executor.d.ts +41 -0
  205. package/dist/git-push-executor.d.ts.map +1 -0
  206. package/dist/git-push-executor.js +142 -0
  207. package/dist/git-push-executor.js.map +1 -0
  208. package/dist/glob.d.ts +26 -0
  209. package/dist/glob.d.ts.map +1 -0
  210. package/dist/glob.js +93 -0
  211. package/dist/glob.js.map +1 -0
  212. package/dist/goal-adaptive.d.ts +295 -0
  213. package/dist/goal-adaptive.d.ts.map +1 -0
  214. package/dist/goal-adaptive.js +393 -0
  215. package/dist/goal-adaptive.js.map +1 -0
  216. package/dist/goal-compile.d.ts +342 -0
  217. package/dist/goal-compile.d.ts.map +1 -0
  218. package/dist/goal-compile.js +444 -0
  219. package/dist/goal-compile.js.map +1 -0
  220. package/dist/goal-depends-executor.d.ts +55 -0
  221. package/dist/goal-depends-executor.d.ts.map +1 -0
  222. package/dist/goal-depends-executor.js +202 -0
  223. package/dist/goal-depends-executor.js.map +1 -0
  224. package/dist/goal-execution-evidence.d.ts +135 -0
  225. package/dist/goal-execution-evidence.d.ts.map +1 -0
  226. package/dist/goal-execution-evidence.js +159 -0
  227. package/dist/goal-execution-evidence.js.map +1 -0
  228. package/dist/goal-gap.d.ts +140 -0
  229. package/dist/goal-gap.d.ts.map +1 -0
  230. package/dist/goal-gap.js +197 -0
  231. package/dist/goal-gap.js.map +1 -0
  232. package/dist/goal-met-target.d.ts +98 -0
  233. package/dist/goal-met-target.d.ts.map +1 -0
  234. package/dist/goal-met-target.js +92 -0
  235. package/dist/goal-met-target.js.map +1 -0
  236. package/dist/goal-ownership.d.ts +416 -0
  237. package/dist/goal-ownership.d.ts.map +1 -0
  238. package/dist/goal-ownership.js +576 -0
  239. package/dist/goal-ownership.js.map +1 -0
  240. package/dist/goal-runs.d.ts +82 -0
  241. package/dist/goal-runs.d.ts.map +1 -0
  242. package/dist/goal-runs.js +120 -0
  243. package/dist/goal-runs.js.map +1 -0
  244. package/dist/goal-store.d.ts +146 -0
  245. package/dist/goal-store.d.ts.map +1 -0
  246. package/dist/goal-store.js +294 -0
  247. package/dist/goal-store.js.map +1 -0
  248. package/dist/goal-view.d.ts +156 -0
  249. package/dist/goal-view.d.ts.map +1 -0
  250. package/dist/goal-view.js +499 -0
  251. package/dist/goal-view.js.map +1 -0
  252. package/dist/governed-approvals.d.ts +152 -0
  253. package/dist/governed-approvals.d.ts.map +1 -0
  254. package/dist/governed-approvals.js +246 -0
  255. package/dist/governed-approvals.js.map +1 -0
  256. package/dist/governed-broker.d.ts +169 -0
  257. package/dist/governed-broker.d.ts.map +1 -0
  258. package/dist/governed-broker.js +196 -0
  259. package/dist/governed-broker.js.map +1 -0
  260. package/dist/governed-run.d.ts +220 -0
  261. package/dist/governed-run.d.ts.map +1 -0
  262. package/dist/governed-run.js +406 -0
  263. package/dist/governed-run.js.map +1 -0
  264. package/dist/grant-reconcile.d.ts +63 -0
  265. package/dist/grant-reconcile.d.ts.map +1 -0
  266. package/dist/grant-reconcile.js +121 -0
  267. package/dist/grant-reconcile.js.map +1 -0
  268. package/dist/index-for-head.d.ts +56 -0
  269. package/dist/index-for-head.d.ts.map +1 -0
  270. package/dist/index-for-head.js +103 -0
  271. package/dist/index-for-head.js.map +1 -0
  272. package/dist/index.d.ts +39 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +55 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/init-detect.d.ts +39 -0
  277. package/dist/init-detect.d.ts.map +1 -0
  278. package/dist/init-detect.js +188 -0
  279. package/dist/init-detect.js.map +1 -0
  280. package/dist/integration-deps.d.ts +62 -0
  281. package/dist/integration-deps.d.ts.map +1 -0
  282. package/dist/integration-deps.js +251 -0
  283. package/dist/integration-deps.js.map +1 -0
  284. package/dist/integration-runtime.d.ts +458 -0
  285. package/dist/integration-runtime.d.ts.map +1 -0
  286. package/dist/integration-runtime.js +681 -0
  287. package/dist/integration-runtime.js.map +1 -0
  288. package/dist/integration.d.ts +45 -0
  289. package/dist/integration.d.ts.map +1 -0
  290. package/dist/integration.js +87 -0
  291. package/dist/integration.js.map +1 -0
  292. package/dist/integrator.d.ts +84 -0
  293. package/dist/integrator.d.ts.map +1 -0
  294. package/dist/integrator.js +193 -0
  295. package/dist/integrator.js.map +1 -0
  296. package/dist/lane-claims.d.ts +174 -0
  297. package/dist/lane-claims.d.ts.map +1 -0
  298. package/dist/lane-claims.js +218 -0
  299. package/dist/lane-claims.js.map +1 -0
  300. package/dist/lane-coverage.d.ts +87 -0
  301. package/dist/lane-coverage.d.ts.map +1 -0
  302. package/dist/lane-coverage.js +129 -0
  303. package/dist/lane-coverage.js.map +1 -0
  304. package/dist/ledger-replay.d.ts +115 -0
  305. package/dist/ledger-replay.d.ts.map +1 -0
  306. package/dist/ledger-replay.js +155 -0
  307. package/dist/ledger-replay.js.map +1 -0
  308. package/dist/ledger-store.d.ts +68 -0
  309. package/dist/ledger-store.d.ts.map +1 -0
  310. package/dist/ledger-store.js +111 -0
  311. package/dist/ledger-store.js.map +1 -0
  312. package/dist/loop-safety-observer.d.ts +132 -0
  313. package/dist/loop-safety-observer.d.ts.map +1 -0
  314. package/dist/loop-safety-observer.js +76 -0
  315. package/dist/loop-safety-observer.js.map +1 -0
  316. package/dist/loop-safety-store.d.ts +339 -0
  317. package/dist/loop-safety-store.d.ts.map +1 -0
  318. package/dist/loop-safety-store.js +800 -0
  319. package/dist/loop-safety-store.js.map +1 -0
  320. package/dist/machine-store.d.ts +54 -0
  321. package/dist/machine-store.d.ts.map +1 -0
  322. package/dist/machine-store.js +149 -0
  323. package/dist/machine-store.js.map +1 -0
  324. package/dist/mcp-main.d.ts +2 -0
  325. package/dist/mcp-main.d.ts.map +1 -0
  326. package/dist/mcp-main.js +116 -0
  327. package/dist/mcp-main.js.map +1 -0
  328. package/dist/mcp-server.d.ts +72 -0
  329. package/dist/mcp-server.d.ts.map +1 -0
  330. package/dist/mcp-server.js +259 -0
  331. package/dist/mcp-server.js.map +1 -0
  332. package/dist/model-routing.d.ts +75 -0
  333. package/dist/model-routing.d.ts.map +1 -0
  334. package/dist/model-routing.js +108 -0
  335. package/dist/model-routing.js.map +1 -0
  336. package/dist/model-transport.d.ts +188 -0
  337. package/dist/model-transport.d.ts.map +1 -0
  338. package/dist/model-transport.js +625 -0
  339. package/dist/model-transport.js.map +1 -0
  340. package/dist/notify-cadence.d.ts +55 -0
  341. package/dist/notify-cadence.d.ts.map +1 -0
  342. package/dist/notify-cadence.js +68 -0
  343. package/dist/notify-cadence.js.map +1 -0
  344. package/dist/notify-channel.d.ts +69 -0
  345. package/dist/notify-channel.d.ts.map +1 -0
  346. package/dist/notify-channel.js +151 -0
  347. package/dist/notify-channel.js.map +1 -0
  348. package/dist/notify.d.ts +71 -0
  349. package/dist/notify.d.ts.map +1 -0
  350. package/dist/notify.js +125 -0
  351. package/dist/notify.js.map +1 -0
  352. package/dist/observation-inventory.d.ts +65 -0
  353. package/dist/observation-inventory.d.ts.map +1 -0
  354. package/dist/observation-inventory.js +139 -0
  355. package/dist/observation-inventory.js.map +1 -0
  356. package/dist/owned-children.d.ts +45 -0
  357. package/dist/owned-children.d.ts.map +1 -0
  358. package/dist/owned-children.js +64 -0
  359. package/dist/owned-children.js.map +1 -0
  360. package/dist/packet-for-goal.d.ts +19 -0
  361. package/dist/packet-for-goal.d.ts.map +1 -0
  362. package/dist/packet-for-goal.js +32 -0
  363. package/dist/packet-for-goal.js.map +1 -0
  364. package/dist/pair-cmd.d.ts +85 -0
  365. package/dist/pair-cmd.d.ts.map +1 -0
  366. package/dist/pair-cmd.js +205 -0
  367. package/dist/pair-cmd.js.map +1 -0
  368. package/dist/partial-asker.d.ts +96 -0
  369. package/dist/partial-asker.d.ts.map +1 -0
  370. package/dist/partial-asker.js +117 -0
  371. package/dist/partial-asker.js.map +1 -0
  372. package/dist/pdf-text-lines.d.ts +17 -0
  373. package/dist/pdf-text-lines.d.ts.map +1 -0
  374. package/dist/pdf-text-lines.js +70 -0
  375. package/dist/pdf-text-lines.js.map +1 -0
  376. package/dist/pdf-text-worker.d.ts +2 -0
  377. package/dist/pdf-text-worker.d.ts.map +1 -0
  378. package/dist/pdf-text-worker.js +81 -0
  379. package/dist/pdf-text-worker.js.map +1 -0
  380. package/dist/pdf-text.d.ts +56 -0
  381. package/dist/pdf-text.d.ts.map +1 -0
  382. package/dist/pdf-text.js +64 -0
  383. package/dist/pdf-text.js.map +1 -0
  384. package/dist/pending-action.d.ts +154 -0
  385. package/dist/pending-action.d.ts.map +1 -0
  386. package/dist/pending-action.js +148 -0
  387. package/dist/pending-action.js.map +1 -0
  388. package/dist/preemptor.d.ts +40 -0
  389. package/dist/preemptor.d.ts.map +1 -0
  390. package/dist/preemptor.js +49 -0
  391. package/dist/preemptor.js.map +1 -0
  392. package/dist/programme-mode.d.ts +29 -0
  393. package/dist/programme-mode.d.ts.map +1 -0
  394. package/dist/programme-mode.js +50 -0
  395. package/dist/programme-mode.js.map +1 -0
  396. package/dist/progress-view.d.ts +14 -0
  397. package/dist/progress-view.d.ts.map +1 -0
  398. package/dist/progress-view.js +114 -0
  399. package/dist/progress-view.js.map +1 -0
  400. package/dist/project-graph-builder.d.ts +81 -0
  401. package/dist/project-graph-builder.d.ts.map +1 -0
  402. package/dist/project-graph-builder.js +246 -0
  403. package/dist/project-graph-builder.js.map +1 -0
  404. package/dist/project-graph-runtime.d.ts +86 -0
  405. package/dist/project-graph-runtime.d.ts.map +1 -0
  406. package/dist/project-graph-runtime.js +112 -0
  407. package/dist/project-graph-runtime.js.map +1 -0
  408. package/dist/project-index-builder.d.ts +96 -0
  409. package/dist/project-index-builder.d.ts.map +1 -0
  410. package/dist/project-index-builder.js +298 -0
  411. package/dist/project-index-builder.js.map +1 -0
  412. package/dist/project-index-facts.d.ts +51 -0
  413. package/dist/project-index-facts.d.ts.map +1 -0
  414. package/dist/project-index-facts.js +117 -0
  415. package/dist/project-index-facts.js.map +1 -0
  416. package/dist/project-index-store.d.ts +97 -0
  417. package/dist/project-index-store.d.ts.map +1 -0
  418. package/dist/project-index-store.js +140 -0
  419. package/dist/project-index-store.js.map +1 -0
  420. package/dist/project-memory-store.d.ts +127 -0
  421. package/dist/project-memory-store.d.ts.map +1 -0
  422. package/dist/project-memory-store.js +302 -0
  423. package/dist/project-memory-store.js.map +1 -0
  424. package/dist/proposal-store.d.ts +14 -0
  425. package/dist/proposal-store.d.ts.map +1 -0
  426. package/dist/proposal-store.js +19 -0
  427. package/dist/proposal-store.js.map +1 -0
  428. package/dist/prover.d.ts +80 -0
  429. package/dist/prover.d.ts.map +1 -0
  430. package/dist/prover.js +137 -0
  431. package/dist/prover.js.map +1 -0
  432. package/dist/provider-identity-store.d.ts +34 -0
  433. package/dist/provider-identity-store.d.ts.map +1 -0
  434. package/dist/provider-identity-store.js +72 -0
  435. package/dist/provider-identity-store.js.map +1 -0
  436. package/dist/provider-identity.d.ts +104 -0
  437. package/dist/provider-identity.d.ts.map +1 -0
  438. package/dist/provider-identity.js +167 -0
  439. package/dist/provider-identity.js.map +1 -0
  440. package/dist/provider-limit.d.ts +79 -0
  441. package/dist/provider-limit.d.ts.map +1 -0
  442. package/dist/provider-limit.js +148 -0
  443. package/dist/provider-limit.js.map +1 -0
  444. package/dist/provider-preflight.d.ts +171 -0
  445. package/dist/provider-preflight.d.ts.map +1 -0
  446. package/dist/provider-preflight.js +395 -0
  447. package/dist/provider-preflight.js.map +1 -0
  448. package/dist/provider-state.d.ts +120 -0
  449. package/dist/provider-state.d.ts.map +1 -0
  450. package/dist/provider-state.js +182 -0
  451. package/dist/provider-state.js.map +1 -0
  452. package/dist/providers.d.ts +132 -0
  453. package/dist/providers.d.ts.map +1 -0
  454. package/dist/providers.js +161 -0
  455. package/dist/providers.js.map +1 -0
  456. package/dist/publication-recorder.d.ts +117 -0
  457. package/dist/publication-recorder.d.ts.map +1 -0
  458. package/dist/publication-recorder.js +124 -0
  459. package/dist/publication-recorder.js.map +1 -0
  460. package/dist/pursuit-delivery.d.ts +24 -0
  461. package/dist/pursuit-delivery.d.ts.map +1 -0
  462. package/dist/pursuit-delivery.js +38 -0
  463. package/dist/pursuit-delivery.js.map +1 -0
  464. package/dist/queue-store.d.ts +132 -0
  465. package/dist/queue-store.d.ts.map +1 -0
  466. package/dist/queue-store.js +185 -0
  467. package/dist/queue-store.js.map +1 -0
  468. package/dist/queue-summary.d.ts +14 -0
  469. package/dist/queue-summary.d.ts.map +1 -0
  470. package/dist/queue-summary.js +82 -0
  471. package/dist/queue-summary.js.map +1 -0
  472. package/dist/queue-view.d.ts +20 -0
  473. package/dist/queue-view.d.ts.map +1 -0
  474. package/dist/queue-view.js +163 -0
  475. package/dist/queue-view.js.map +1 -0
  476. package/dist/queue-write-lease.d.ts +106 -0
  477. package/dist/queue-write-lease.d.ts.map +1 -0
  478. package/dist/queue-write-lease.js +282 -0
  479. package/dist/queue-write-lease.js.map +1 -0
  480. package/dist/rails-home.d.ts +162 -0
  481. package/dist/rails-home.d.ts.map +1 -0
  482. package/dist/rails-home.js +452 -0
  483. package/dist/rails-home.js.map +1 -0
  484. package/dist/rails-migrations.d.ts +75 -0
  485. package/dist/rails-migrations.d.ts.map +1 -0
  486. package/dist/rails-migrations.js +199 -0
  487. package/dist/rails-migrations.js.map +1 -0
  488. package/dist/rails-stores.d.ts +49 -0
  489. package/dist/rails-stores.d.ts.map +1 -0
  490. package/dist/rails-stores.js +100 -0
  491. package/dist/rails-stores.js.map +1 -0
  492. package/dist/reaper.d.ts +69 -0
  493. package/dist/reaper.d.ts.map +1 -0
  494. package/dist/reaper.js +152 -0
  495. package/dist/reaper.js.map +1 -0
  496. package/dist/record-store.d.ts +36 -0
  497. package/dist/record-store.d.ts.map +1 -0
  498. package/dist/record-store.js +78 -0
  499. package/dist/record-store.js.map +1 -0
  500. package/dist/recovery-io.d.ts +50 -0
  501. package/dist/recovery-io.d.ts.map +1 -0
  502. package/dist/recovery-io.js +122 -0
  503. package/dist/recovery-io.js.map +1 -0
  504. package/dist/recovery.d.ts +146 -0
  505. package/dist/recovery.d.ts.map +1 -0
  506. package/dist/recovery.js +110 -0
  507. package/dist/recovery.js.map +1 -0
  508. package/dist/redraw.d.ts +34 -0
  509. package/dist/redraw.d.ts.map +1 -0
  510. package/dist/redraw.js +62 -0
  511. package/dist/redraw.js.map +1 -0
  512. package/dist/rejected-evidence.d.ts +255 -0
  513. package/dist/rejected-evidence.d.ts.map +1 -0
  514. package/dist/rejected-evidence.js +324 -0
  515. package/dist/rejected-evidence.js.map +1 -0
  516. package/dist/report-cmd.d.ts +32 -0
  517. package/dist/report-cmd.d.ts.map +1 -0
  518. package/dist/report-cmd.js +201 -0
  519. package/dist/report-cmd.js.map +1 -0
  520. package/dist/repos-cmd.d.ts +80 -0
  521. package/dist/repos-cmd.d.ts.map +1 -0
  522. package/dist/repos-cmd.js +240 -0
  523. package/dist/repos-cmd.js.map +1 -0
  524. package/dist/request-body-shape.d.ts +93 -0
  525. package/dist/request-body-shape.d.ts.map +1 -0
  526. package/dist/request-body-shape.js +407 -0
  527. package/dist/request-body-shape.js.map +1 -0
  528. package/dist/requirement-claims.d.ts +89 -0
  529. package/dist/requirement-claims.d.ts.map +1 -0
  530. package/dist/requirement-claims.js +92 -0
  531. package/dist/requirement-claims.js.map +1 -0
  532. package/dist/retention-apply-runtime.d.ts +105 -0
  533. package/dist/retention-apply-runtime.d.ts.map +1 -0
  534. package/dist/retention-apply-runtime.js +269 -0
  535. package/dist/retention-apply-runtime.js.map +1 -0
  536. package/dist/reverify.d.ts +41 -0
  537. package/dist/reverify.d.ts.map +1 -0
  538. package/dist/reverify.js +69 -0
  539. package/dist/reverify.js.map +1 -0
  540. package/dist/round-records.d.ts +65 -0
  541. package/dist/round-records.d.ts.map +1 -0
  542. package/dist/round-records.js +61 -0
  543. package/dist/round-records.js.map +1 -0
  544. package/dist/round-runner.d.ts +924 -0
  545. package/dist/round-runner.d.ts.map +1 -0
  546. package/dist/round-runner.js +1693 -0
  547. package/dist/round-runner.js.map +1 -0
  548. package/dist/run-approval.d.ts +180 -0
  549. package/dist/run-approval.d.ts.map +1 -0
  550. package/dist/run-approval.js +199 -0
  551. package/dist/run-approval.js.map +1 -0
  552. package/dist/run-cmd.d.ts +359 -0
  553. package/dist/run-cmd.d.ts.map +1 -0
  554. package/dist/run-cmd.js +4418 -0
  555. package/dist/run-cmd.js.map +1 -0
  556. package/dist/run-delivery.d.ts +71 -0
  557. package/dist/run-delivery.d.ts.map +1 -0
  558. package/dist/run-delivery.js +104 -0
  559. package/dist/run-delivery.js.map +1 -0
  560. package/dist/run-ended.d.ts +75 -0
  561. package/dist/run-ended.d.ts.map +1 -0
  562. package/dist/run-ended.js +27 -0
  563. package/dist/run-ended.js.map +1 -0
  564. package/dist/run-ending-locator.d.ts +125 -0
  565. package/dist/run-ending-locator.d.ts.map +1 -0
  566. package/dist/run-ending-locator.js +133 -0
  567. package/dist/run-ending-locator.js.map +1 -0
  568. package/dist/run-launcher.d.ts +459 -0
  569. package/dist/run-launcher.d.ts.map +1 -0
  570. package/dist/run-launcher.js +652 -0
  571. package/dist/run-launcher.js.map +1 -0
  572. package/dist/run-lease.d.ts +208 -0
  573. package/dist/run-lease.d.ts.map +1 -0
  574. package/dist/run-lease.js +301 -0
  575. package/dist/run-lease.js.map +1 -0
  576. package/dist/run-refusal-marks.d.ts +189 -0
  577. package/dist/run-refusal-marks.d.ts.map +1 -0
  578. package/dist/run-refusal-marks.js +249 -0
  579. package/dist/run-refusal-marks.js.map +1 -0
  580. package/dist/run-start-marks.d.ts +153 -0
  581. package/dist/run-start-marks.d.ts.map +1 -0
  582. package/dist/run-start-marks.js +269 -0
  583. package/dist/run-start-marks.js.map +1 -0
  584. package/dist/scan-cmd.d.ts +14 -0
  585. package/dist/scan-cmd.d.ts.map +1 -0
  586. package/dist/scan-cmd.js +270 -0
  587. package/dist/scan-cmd.js.map +1 -0
  588. package/dist/scan-events.d.ts +50 -0
  589. package/dist/scan-events.d.ts.map +1 -0
  590. package/dist/scan-events.js +91 -0
  591. package/dist/scan-events.js.map +1 -0
  592. package/dist/scan.d.ts +191 -0
  593. package/dist/scan.d.ts.map +1 -0
  594. package/dist/scan.js +417 -0
  595. package/dist/scan.js.map +1 -0
  596. package/dist/scope-rule.d.ts +148 -0
  597. package/dist/scope-rule.d.ts.map +1 -0
  598. package/dist/scope-rule.js +121 -0
  599. package/dist/scope-rule.js.map +1 -0
  600. package/dist/self-repo-id.d.ts +18 -0
  601. package/dist/self-repo-id.d.ts.map +1 -0
  602. package/dist/self-repo-id.js +70 -0
  603. package/dist/self-repo-id.js.map +1 -0
  604. package/dist/server-client.d.ts +766 -0
  605. package/dist/server-client.d.ts.map +1 -0
  606. package/dist/server-client.js +475 -0
  607. package/dist/server-client.js.map +1 -0
  608. package/dist/session-adapter-runtime.d.ts +195 -0
  609. package/dist/session-adapter-runtime.d.ts.map +1 -0
  610. package/dist/session-adapter-runtime.js +373 -0
  611. package/dist/session-adapter-runtime.js.map +1 -0
  612. package/dist/session-checkpoint-runtime.d.ts +62 -0
  613. package/dist/session-checkpoint-runtime.d.ts.map +1 -0
  614. package/dist/session-checkpoint-runtime.js +268 -0
  615. package/dist/session-checkpoint-runtime.js.map +1 -0
  616. package/dist/session-control-transport.d.ts +106 -0
  617. package/dist/session-control-transport.d.ts.map +1 -0
  618. package/dist/session-control-transport.js +232 -0
  619. package/dist/session-control-transport.js.map +1 -0
  620. package/dist/session-observation-store.d.ts +98 -0
  621. package/dist/session-observation-store.d.ts.map +1 -0
  622. package/dist/session-observation-store.js +155 -0
  623. package/dist/session-observation-store.js.map +1 -0
  624. package/dist/session-observe-runtime.d.ts +123 -0
  625. package/dist/session-observe-runtime.d.ts.map +1 -0
  626. package/dist/session-observe-runtime.js +317 -0
  627. package/dist/session-observe-runtime.js.map +1 -0
  628. package/dist/session-store.d.ts +98 -0
  629. package/dist/session-store.d.ts.map +1 -0
  630. package/dist/session-store.js +116 -0
  631. package/dist/session-store.js.map +1 -0
  632. package/dist/sessions-cmd.d.ts +202 -0
  633. package/dist/sessions-cmd.d.ts.map +1 -0
  634. package/dist/sessions-cmd.js +609 -0
  635. package/dist/sessions-cmd.js.map +1 -0
  636. package/dist/settings-overlay.d.ts +53 -0
  637. package/dist/settings-overlay.d.ts.map +1 -0
  638. package/dist/settings-overlay.js +175 -0
  639. package/dist/settings-overlay.js.map +1 -0
  640. package/dist/setup-prompt.d.ts +34 -0
  641. package/dist/setup-prompt.d.ts.map +1 -0
  642. package/dist/setup-prompt.js +92 -0
  643. package/dist/setup-prompt.js.map +1 -0
  644. package/dist/setup.d.ts +103 -0
  645. package/dist/setup.d.ts.map +1 -0
  646. package/dist/setup.js +283 -0
  647. package/dist/setup.js.map +1 -0
  648. package/dist/shared-grants.d.ts +164 -0
  649. package/dist/shared-grants.d.ts.map +1 -0
  650. package/dist/shared-grants.js +242 -0
  651. package/dist/shared-grants.js.map +1 -0
  652. package/dist/signals.d.ts +55 -0
  653. package/dist/signals.d.ts.map +1 -0
  654. package/dist/signals.js +85 -0
  655. package/dist/signals.js.map +1 -0
  656. package/dist/source-cmd.d.ts +201 -0
  657. package/dist/source-cmd.d.ts.map +1 -0
  658. package/dist/source-cmd.js +684 -0
  659. package/dist/source-cmd.js.map +1 -0
  660. package/dist/source-verdict.d.ts +99 -0
  661. package/dist/source-verdict.d.ts.map +1 -0
  662. package/dist/source-verdict.js +182 -0
  663. package/dist/source-verdict.js.map +1 -0
  664. package/dist/spend-ledger.d.ts +350 -0
  665. package/dist/spend-ledger.d.ts.map +1 -0
  666. package/dist/spend-ledger.js +546 -0
  667. package/dist/spend-ledger.js.map +1 -0
  668. package/dist/stale-run-sweep.d.ts +79 -0
  669. package/dist/stale-run-sweep.d.ts.map +1 -0
  670. package/dist/stale-run-sweep.js +116 -0
  671. package/dist/stale-run-sweep.js.map +1 -0
  672. package/dist/start-attempts.d.ts +385 -0
  673. package/dist/start-attempts.d.ts.map +1 -0
  674. package/dist/start-attempts.js +613 -0
  675. package/dist/start-attempts.js.map +1 -0
  676. package/dist/status-cmd.d.ts +28 -0
  677. package/dist/status-cmd.d.ts.map +1 -0
  678. package/dist/status-cmd.js +116 -0
  679. package/dist/status-cmd.js.map +1 -0
  680. package/dist/stop-follow-up.d.ts +107 -0
  681. package/dist/stop-follow-up.d.ts.map +1 -0
  682. package/dist/stop-follow-up.js +172 -0
  683. package/dist/stop-follow-up.js.map +1 -0
  684. package/dist/surface-lease.d.ts +118 -0
  685. package/dist/surface-lease.d.ts.map +1 -0
  686. package/dist/surface-lease.js +485 -0
  687. package/dist/surface-lease.js.map +1 -0
  688. package/dist/sync.d.ts +54 -0
  689. package/dist/sync.d.ts.map +1 -0
  690. package/dist/sync.js +93 -0
  691. package/dist/sync.js.map +1 -0
  692. package/dist/syncing-ledger.d.ts +45 -0
  693. package/dist/syncing-ledger.d.ts.map +1 -0
  694. package/dist/syncing-ledger.js +53 -0
  695. package/dist/syncing-ledger.js.map +1 -0
  696. package/dist/temp-fixtures.d.ts +68 -0
  697. package/dist/temp-fixtures.d.ts.map +1 -0
  698. package/dist/temp-fixtures.js +38 -0
  699. package/dist/temp-fixtures.js.map +1 -0
  700. package/dist/token-window.d.ts +84 -0
  701. package/dist/token-window.d.ts.map +1 -0
  702. package/dist/token-window.js +143 -0
  703. package/dist/token-window.js.map +1 -0
  704. package/dist/tool-bridge.d.ts +177 -0
  705. package/dist/tool-bridge.d.ts.map +1 -0
  706. package/dist/tool-bridge.js +329 -0
  707. package/dist/tool-bridge.js.map +1 -0
  708. package/dist/tool-call-record.d.ts +197 -0
  709. package/dist/tool-call-record.d.ts.map +1 -0
  710. package/dist/tool-call-record.js +276 -0
  711. package/dist/tool-call-record.js.map +1 -0
  712. package/dist/tool-protocol.d.ts +135 -0
  713. package/dist/tool-protocol.d.ts.map +1 -0
  714. package/dist/tool-protocol.js +104 -0
  715. package/dist/tool-protocol.js.map +1 -0
  716. package/dist/tool-worker.d.ts +3 -0
  717. package/dist/tool-worker.d.ts.map +1 -0
  718. package/dist/tool-worker.js +478 -0
  719. package/dist/tool-worker.js.map +1 -0
  720. package/dist/toolchain-mirror.d.ts +86 -0
  721. package/dist/toolchain-mirror.d.ts.map +1 -0
  722. package/dist/toolchain-mirror.js +307 -0
  723. package/dist/toolchain-mirror.js.map +1 -0
  724. package/dist/trusted-git.d.ts +243 -0
  725. package/dist/trusted-git.d.ts.map +1 -0
  726. package/dist/trusted-git.js +920 -0
  727. package/dist/trusted-git.js.map +1 -0
  728. package/dist/ui.d.ts +76 -0
  729. package/dist/ui.d.ts.map +1 -0
  730. package/dist/ui.js +118 -0
  731. package/dist/ui.js.map +1 -0
  732. package/dist/untrusted-exec.d.ts +275 -0
  733. package/dist/untrusted-exec.d.ts.map +1 -0
  734. package/dist/untrusted-exec.js +479 -0
  735. package/dist/untrusted-exec.js.map +1 -0
  736. package/dist/untrusted-gate.d.ts +73 -0
  737. package/dist/untrusted-gate.d.ts.map +1 -0
  738. package/dist/untrusted-gate.js +226 -0
  739. package/dist/untrusted-gate.js.map +1 -0
  740. package/dist/uploader.d.ts +153 -0
  741. package/dist/uploader.d.ts.map +1 -0
  742. package/dist/uploader.js +204 -0
  743. package/dist/uploader.js.map +1 -0
  744. package/dist/verifier.d.ts +66 -0
  745. package/dist/verifier.d.ts.map +1 -0
  746. package/dist/verifier.js +170 -0
  747. package/dist/verifier.js.map +1 -0
  748. package/dist/verify-apply.d.ts +26 -0
  749. package/dist/verify-apply.d.ts.map +1 -0
  750. package/dist/verify-apply.js +36 -0
  751. package/dist/verify-apply.js.map +1 -0
  752. package/dist/verify-cmd.d.ts +44 -0
  753. package/dist/verify-cmd.d.ts.map +1 -0
  754. package/dist/verify-cmd.js +125 -0
  755. package/dist/verify-cmd.js.map +1 -0
  756. package/dist/verify-pass.d.ts +73 -0
  757. package/dist/verify-pass.d.ts.map +1 -0
  758. package/dist/verify-pass.js +103 -0
  759. package/dist/verify-pass.js.map +1 -0
  760. package/dist/welcome.d.ts +43 -0
  761. package/dist/welcome.d.ts.map +1 -0
  762. package/dist/welcome.js +116 -0
  763. package/dist/welcome.js.map +1 -0
  764. package/dist/workspace-provisioner.d.ts +65 -0
  765. package/dist/workspace-provisioner.d.ts.map +1 -0
  766. package/dist/workspace-provisioner.js +66 -0
  767. package/dist/workspace-provisioner.js.map +1 -0
  768. package/dist/workspace-quarantine.d.ts +64 -0
  769. package/dist/workspace-quarantine.d.ts.map +1 -0
  770. package/dist/workspace-quarantine.js +156 -0
  771. package/dist/workspace-quarantine.js.map +1 -0
  772. package/dist/workspace-residue-sweep.d.ts +113 -0
  773. package/dist/workspace-residue-sweep.d.ts.map +1 -0
  774. package/dist/workspace-residue-sweep.js +158 -0
  775. package/dist/workspace-residue-sweep.js.map +1 -0
  776. package/dist/workspace.d.ts +639 -0
  777. package/dist/workspace.d.ts.map +1 -0
  778. package/dist/workspace.js +2026 -0
  779. package/dist/workspace.js.map +1 -0
  780. package/package.json +54 -6
  781. package/win/CredProbe.cs +64 -0
  782. package/win/FakeClaude.cs +56 -0
  783. package/win/HandleProbe.cs +116 -0
  784. package/win/TokenProbe.cs +53 -0
  785. package/win/appcontainer.ps1 +500 -0
@@ -0,0 +1,2026 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RunRegistry = exports.WorkspaceError = exports.nodeProbe = exports.WORKSPACES_DIR = void 0;
4
+ exports.forgetCreationMarker = forgetCreationMarker;
5
+ exports.workspacesRoot = workspacesRoot;
6
+ exports.requestControlEverywhere = requestControlEverywhere;
7
+ exports.createWorkspace = createWorkspace;
8
+ exports.reapableCheckout = reapableCheckout;
9
+ exports.reapVerdict = reapVerdict;
10
+ exports.workspaceResidue = workspaceResidue;
11
+ exports.retainEvidence = retainEvidence;
12
+ exports.reapWorkspace = reapWorkspace;
13
+ exports.abandonedWorkspaces = abandonedWorkspaces;
14
+ exports.checkoutBusy = checkoutBusy;
15
+ exports.creationMarkerHolder = creationMarkerHolder;
16
+ exports.clearCreationMarker = clearCreationMarker;
17
+ exports.clearAddressedControl = clearAddressedControl;
18
+ exports.reapAbandoned = reapAbandoned;
19
+ const node_fs_1 = require("node:fs");
20
+ const checkout_carry_1 = require("./checkout-carry");
21
+ const node_crypto_1 = require("node:crypto");
22
+ const node_os_1 = require("node:os");
23
+ const node_path_1 = require("node:path");
24
+ const git_driver_1 = require("./git-driver");
25
+ const trusted_git_1 = require("./trusted-git");
26
+ const run_lease_1 = require("./run-lease");
27
+ const control_file_1 = require("./control-file");
28
+ const workspace_residue_sweep_1 = require("./workspace-residue-sweep");
29
+ const workspace_quarantine_1 = require("./workspace-quarantine");
30
+ /**
31
+ * WHERE A SECOND EXECUTION LIVES, AND WHO IS ALLOWED TO SAY SO.
32
+ *
33
+ * Phase 1 makes two executions safe by giving each its own checkout and its own
34
+ * execution state directory, rather than by making a dozen single-writer stores
35
+ * multi-writer. That is the cheaper half of a decision the recon argued at
36
+ * length: every store already takes `stateDir` as its first argument, so N state
37
+ * directories are already expressible — what was missing was a place to put
38
+ * them and something that knows they exist.
39
+ *
40
+ * TWO THINGS LIVE HERE AND THEY ARE DELIBERATELY SEPARATE:
41
+ *
42
+ * the WORKSPACE a linked git worktree plus its own `.looply/state`. Created
43
+ * through `GitDriver`, so it inherits every Phase 0/0.5
44
+ * defence rather than reimplementing a safe-enough subset.
45
+ * the REGISTRY one exclusively-created file per live execution, under the
46
+ * PROJECT's state directory. It is what makes capacity
47
+ * countable, `status` aggregable, and an orphan identifiable.
48
+ *
49
+ * ⚠️ THE REGISTRY IS NOT A LOCK. The run lease is still the mutual exclusion,
50
+ * still one per execution state directory, and still the thing that decides
51
+ * whether a checkout is busy. The registry answers a different question — "what
52
+ * else is running for this project" — which no lease can answer because leases
53
+ * cannot see each other.
54
+ */
55
+ /** How a workspace's own directory is named under the workspaces root. */
56
+ exports.WORKSPACES_DIR = 'workspaces';
57
+ /**
58
+ * Clear a workspace's creation marker, by name.
59
+ *
60
+ * 🔴 A MARKER WHOSE PID IS RECYCLED PINS ITS WORKSPACE AS "in use" FOR EVER.
61
+ * `looply runs` shows it, `--reap` refuses it because something appears to be
62
+ * using it, and nothing else can ever say otherwise — the same permanent wedge
63
+ * a recycled pid makes of a slot, in a place with no `--release` to reach it.
64
+ *
65
+ * ⚠️ IT CLEARS THE MARKER, NOT THE CHECKOUT. Afterwards the workspace is
66
+ * ordinary: `--reap` judges it on residue and evidence like any other, and will
67
+ * still keep it if it holds a person's files.
68
+ */
69
+ function forgetCreationMarker(cwd, name) {
70
+ /**
71
+ * ⚠️ A DIRECTORY BASENAME, NOT A RUN ID. This used `safeRunId`, which THREW on
72
+ * anything outside `[A-Za-z0-9_-]` — so a directory under the workspaces root
73
+ * with a dot in its name was both listed as busy and permanently unclearable,
74
+ * and typing its name crashed the command instead of answering it.
75
+ *
76
+ * The requirement is only that it name ONE directory directly under the
77
+ * workspaces root: no separators, nothing that could climb.
78
+ */
79
+ if (name === '' || name === '.' || name === '..' || /[\/]/.test(name)) {
80
+ throw new WorkspaceError(`${JSON.stringify(name)} is not the name of a workspace`);
81
+ }
82
+ const at = (0, node_path_1.join)(workspacesRoot(cwd), name, git_driver_1.STATE_DIR, 'state', CREATING);
83
+ try {
84
+ (0, node_fs_1.unlinkSync)(at);
85
+ return true;
86
+ }
87
+ catch (e) {
88
+ if (e.code === 'ENOENT')
89
+ return false;
90
+ throw e;
91
+ }
92
+ }
93
+ /**
94
+ * Where every workspace for a project lives — OUTSIDE the repository.
95
+ *
96
+ * 🔴 IT WAS INSIDE, AND THAT WAS WRONG. The first version put workspaces under
97
+ * `<root>/.looply/workspaces`, and `.gitignore` covers `.looply/state/` and
98
+ * `.looply/runs/` but not that. Measured: creating one workspace turned the main
99
+ * checkout's `git status --porcelain` into `?? .looply/`. Looply's own dirty
100
+ * check would not have noticed — `changedPaths` filters `.looply/**` — but a
101
+ * PERSON would, and a person who then typed `git add -A` would commit an entire
102
+ * second checkout into their repository.
103
+ *
104
+ * `~/.looply` is where this codebase already puts state that must survive the
105
+ * repository, and for the same stated reason: `git clean -fd` would delete
106
+ * in-tree credentials. A workspace has exactly that property.
107
+ *
108
+ * ⚠️ KEYED BY A HASH OF THE MAIN WORKTREE PATH, not by the project's name. Two
109
+ * checkouts of the same repository in two places are two projects here, which is
110
+ * the same identity rule `containerName` and `providerSessionDir` already use.
111
+ */
112
+ function workspacesRoot(cwd, home = (0, node_os_1.homedir)()) {
113
+ /**
114
+ * 🔴 THE SAME IDENTITY THE REGISTRY USES, and it briefly was not.
115
+ *
116
+ * `projectStateDir` learned to fall back to the common dir for a repository
117
+ * with no main worktree — `clone --bare` plus two `worktree add`s — so those
118
+ * checkouts share ONE registry. This still keyed on `mainWorktreeRoot ??
119
+ * cwd`, which is null there, so each checkout got its OWN workspaces root
120
+ * while sharing the records. A record written from checkout A then named a
121
+ * path under A's root, and a `--reap` run from B computed B's root and
122
+ * refused it as "not under this project's workspaces root" — for ever, with a
123
+ * reason that was true and useless.
124
+ *
125
+ * ⚠️ AN ORDINARY CHECKOUT AND A LINKED WORKTREE ARE UNCHANGED:
126
+ * `repoIdentityPath` answers the main worktree root for both, which is
127
+ * exactly what this hashed before.
128
+ */
129
+ const root = (0, trusted_git_1.repoIdentityPath)(cwd);
130
+ const key = (0, node_crypto_1.createHash)('sha256').update(root.toLowerCase(), 'utf8').digest('hex').slice(0, 16);
131
+ return (0, node_path_1.join)(home, '.looply', exports.WORKSPACES_DIR, key);
132
+ }
133
+ exports.nodeProbe = run_lease_1.nodeProbe;
134
+ /**
135
+ * Errno values that mean "somebody else got there", not "something is broken".
136
+ *
137
+ * 🔴 `EEXIST` ALONE IS NOT ENOUGH ON WINDOWS, and this was measured rather than
138
+ * assumed. An exclusive create against a path another process is deleting at
139
+ * that instant fails with **EPERM**, because the file sits in a pending-delete
140
+ * state where it can be neither opened nor re-created. Under 240 concurrent
141
+ * claims it happened 4 times, and each one threw straight out of `claim` and
142
+ * killed the run with a raw filesystem error instead of refusing politely.
143
+ *
144
+ * Every one of these reads as losing a race, which is the conservative answer in
145
+ * all three places it is used: the slot is not taken, the reclaim token is not
146
+ * held, and the stale file is not removed.
147
+ */
148
+ const LOST_THE_RACE = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
149
+ /**
150
+ * A runId that is safe to use as a PATH SEGMENT.
151
+ *
152
+ * ⚠️ RUN IDS REACH THIS FILE FROM TWO UNTRUSTED DIRECTIONS, and both end in a
153
+ * `join` followed by an `unlink` or a recursive copy:
154
+ *
155
+ * from disk a registry record is a JSON file. Anything that can write the
156
+ * project's state directory can put `../../..` in a `runId`.
157
+ * from a person `looply runs --release <name>` passes what was typed straight
158
+ * through to `forget`.
159
+ *
160
+ * `newId('run')` only ever mints `[A-Za-z0-9_]`, so nothing legitimate is
161
+ * refused. This is not defence against the owner of the machine — it is defence
162
+ * against a path escaping the one directory these functions are allowed to
163
+ * touch, which is the same rule the reaper's realpath containment enforces for
164
+ * workspaces.
165
+ */
166
+ const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
167
+ function safeRunId(runId) {
168
+ if (!RUN_ID.test(runId)) {
169
+ throw new WorkspaceError(`${JSON.stringify(runId)} is not a run id this can address`);
170
+ }
171
+ return runId;
172
+ }
173
+ class WorkspaceError extends Error {
174
+ }
175
+ exports.WorkspaceError = WorkspaceError;
176
+ /**
177
+ * The registry of live executions for one project.
178
+ *
179
+ * Entries are one file each, created with `wx`, exactly as the run lease is —
180
+ * the filesystem decides the winner, no read-then-write, and a crash leaves a
181
+ * file that can be judged rather than a lock nobody can clear.
182
+ */
183
+ class RunRegistry {
184
+ projectState;
185
+ probe;
186
+ constructor(projectState, probe = exports.nodeProbe) {
187
+ this.projectState = projectState;
188
+ this.probe = probe;
189
+ }
190
+ get dir() {
191
+ return (0, node_path_1.join)(this.projectState, 'runs');
192
+ }
193
+ pathFor(runId) {
194
+ return (0, node_path_1.join)(this.dir, `${safeRunId(runId)}.json`);
195
+ }
196
+ /**
197
+ * Claim a slot. Throws if this runId is already registered.
198
+ *
199
+ * ⚠️ EXCLUSIVE CREATE, not "check then write". Two executions starting in the
200
+ * same moment must not both believe they took the last slot, and only the
201
+ * filesystem can arbitrate that without a lock of our own.
202
+ */
203
+ register(record) {
204
+ (0, node_fs_1.mkdirSync)(this.dir, { recursive: true });
205
+ try {
206
+ (0, node_fs_1.writeFileSync)(this.pathFor(record.runId), `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx' });
207
+ }
208
+ catch (e) {
209
+ if (e.code === 'EEXIST') {
210
+ throw new WorkspaceError(`this run is already registered: ${record.runId}`);
211
+ }
212
+ throw e;
213
+ }
214
+ }
215
+ /**
216
+ * Take one of `limit` slots, or refuse. THIS is the bound.
217
+ *
218
+ * 🔴 COUNTING WAS CHECK-THEN-ACT AND CLAIMED NOT TO BE. `capacityVerdict`
219
+ * read `active()`, judged, and only then did anything get created — and the
220
+ * exclusive `wx` this class pointed at as "the arbitration" is keyed on
221
+ * `runId`, minted fresh per run. Two runs starting together could never
222
+ * collide on that name, so both passed the count and both registered. Nothing
223
+ * upstream helped: two workspaces hold two different leases, and a lease
224
+ * cannot see another checkout.
225
+ *
226
+ * The SLOT is the exclusive thing. There are at most `limit` slot names, so at
227
+ * most `limit` runs can hold one, and the filesystem decides — exactly as it
228
+ * does for the run lease and the override marker.
229
+ *
230
+ * ⚠️ A SLOT HELD BY A DEAD PROCESS IS RECLAIMED, not counted. Otherwise one
231
+ * crash would lower this machine's concurrency until somebody noticed, which
232
+ * is the capacity leak this exists to avoid.
233
+ */
234
+ claim(record, limit) {
235
+ (0, node_fs_1.mkdirSync)(this.dir, { recursive: true });
236
+ /**
237
+ * 🔴 TAKING A SLOT AND DECIDING WHETHER TO KEEP IT MUST BE ONE STEP.
238
+ *
239
+ * `settle` counts the other holders after winning an exclusive create — the
240
+ * only place the answer cannot be raced by another `wx`. But two claimants
241
+ * BETWEEN their own take and their own count each see a world without the
242
+ * other, and both keep. The first version tried to break that tie by slot
243
+ * number, which works only when the lower-numbered claimant takes first; a
244
+ * holder that vanishes mid-claim reverses the order and both keep.
245
+ *
246
+ * A count cannot be made atomic, so the WINDOW is closed instead: one
247
+ * claimant at a time is between its take and its decision. Contention is
248
+ * brief — a few filesystem calls — and rare, because starting a run is not a
249
+ * hot path.
250
+ *
251
+ * ⚠️ AND THE LOCK IS NOT WHAT MAKES THE BOUND HOLD, which an earlier version
252
+ * of this comment implied. `settle` counts the other holders AFTER its own
253
+ * exclusive create, so a later claimant always sees an earlier one and the
254
+ * total can only ever climb to the limit — without any lock at all. What the
255
+ * lock prevents is the opposite failure: several claimants counting at the
256
+ * same instant, all seeing a full machine, and ALL backing out, leaving it
257
+ * idle below its own limit. That distinction decides what may break a stale
258
+ * lock, and the answer is: anything that cannot name a live owner.
259
+ *
260
+ * ⚠️ AND A LOCK LEFT BY A CRASH IS BROKEN ONLY IF ITS OWNER IS PROVEN DEAD,
261
+ * exactly as the reclaim token is. Otherwise one crash mid-claim would stop
262
+ * this project ever starting a run again.
263
+ */
264
+ const lock = (0, node_path_1.join)(this.dir, 'claim.lock');
265
+ if (!this.waitForToken(lock)) {
266
+ return {
267
+ ok: false,
268
+ because: 'another start is deciding whether it can run, and did not finish. ' +
269
+ 'Try again; if this repeats, `looply runs` will show what is holding on',
270
+ };
271
+ }
272
+ try {
273
+ return this.claimUnderLock(record, limit);
274
+ }
275
+ finally {
276
+ /**
277
+ * 🔴 BY NAME, NOT BY PATH. This unlinked whatever lock was there, so once
278
+ * ANY break had happened — and a measured run of the previous version
279
+ * broke 155 live owners' locks — a second claimant's exit deleted a third
280
+ * claimant's lock, and the loss cascaded: up to three processes were
281
+ * observed inside the critical section at once.
282
+ *
283
+ * Every other release in this class is keyed on identity. This one is now
284
+ * too, for the same reason and by the same rule.
285
+ */
286
+ this.dropToken(lock);
287
+ }
288
+ }
289
+ /** Wait a short while for the claim lock, breaking one a dead process left. */
290
+ waitForToken(lock) {
291
+ const wait = new Int32Array(new SharedArrayBuffer(4));
292
+ for (let attempt = 0; attempt < 100; attempt += 1) {
293
+ if (this.takeToken(lock))
294
+ return true;
295
+ // 20ms, synchronously. `claim` is sync by design — every one of its
296
+ // decisions is a filesystem fact — and this is the only place it waits.
297
+ Atomics.wait(wait, 0, 0, 20);
298
+ }
299
+ return false;
300
+ }
301
+ claimUnderLock(record, limit) {
302
+ const held = [];
303
+ /**
304
+ * ⚠️ THERE IS NO PRE-COUNT, AND THERE WAS ONE.
305
+ *
306
+ * It walked every slot file before the scan, so that slots ABOVE the limit —
307
+ * which the `[0, limit)` scan cannot see — still counted. But a count is not
308
+ * arbitration: several starts could all pass it and then all win different
309
+ * in-range slots, which is how five executions ran at a limit of three.
310
+ *
311
+ * `settle` replaced it, and does the same job on the other side of the
312
+ * exclusive create, where the answer cannot be raced. Keeping both would
313
+ * have left the weaker one able to refuse a start the stronger one would
314
+ * have allowed — a stray slot file turning every start into a refusal
315
+ * BEFORE anything had a chance to judge it.
316
+ */
317
+ for (let slot = 0; slot < limit; slot += 1) {
318
+ const path = this.slotPath(slot);
319
+ if (!(0, node_fs_1.existsSync)(path)) {
320
+ if (this.take(path, record, slot))
321
+ return this.settle(slot, limit, record);
322
+ held.push(this.readSlot(path) ?? 'unreadable');
323
+ continue;
324
+ }
325
+ const taken = this.readSlot(path);
326
+ if (taken === null) {
327
+ /**
328
+ * 🔴 UNREADABLE FAILS CLOSED, AND IT USED TO FAIL OPEN.
329
+ *
330
+ * The old code read "cannot parse" as "nobody is using it", deleted the
331
+ * file and took the slot. But a slot file is written by another process
332
+ * and read without coordination, so the commonest way to see an
333
+ * unparseable one is to catch a LIVE claim mid-write — at which point
334
+ * this deletes a running execution's slot and starts a second run past
335
+ * the limit. The run lease, which this class says it mirrors, fails
336
+ * closed on exactly this and calls the state `ambiguous`.
337
+ *
338
+ * The cost is that an unreadable slot holds capacity until a person
339
+ * clears it. That is why `looply runs` lists slots as well as records,
340
+ * and why `--release slot-<n>` exists.
341
+ */
342
+ held.push('unreadable');
343
+ continue;
344
+ }
345
+ if (this.probe(taken.pid) !== 'dead' && this.leaseAgrees(taken)) {
346
+ // ⚠️ BOTH WITNESSES MUST AGREE IT IS STILL RUNNING. A pid that is alive
347
+ // or unjudgeable is not enough on its own — a recycled pid looks exactly
348
+ // like that — and neither is a lease left behind by a crash. Either one
349
+ // saying 'gone' frees the slot; only both saying 'here' holds it.
350
+ held.push(taken);
351
+ continue;
352
+ }
353
+ if (!this.reclaim(path, taken)) {
354
+ held.push(this.readSlot(path) ?? 'unreadable');
355
+ continue;
356
+ }
357
+ if (this.take(path, record, slot))
358
+ return this.settle(slot, limit, record);
359
+ held.push(this.readSlot(path) ?? 'unreadable');
360
+ }
361
+ /**
362
+ * ⚠️ THE REFUSAL COUNTS EVERY HOLDER, not just the ones the scan walked.
363
+ * The scan sees `[0, limit)`; a person who lowered the limit has holders
364
+ * above it, and a message saying "2 executions and the limit is 2" while
365
+ * three were running would be true about the scan and false about the
366
+ * machine.
367
+ */
368
+ return { ok: false, because: this.atCapacity(this.allHolders(), limit) };
369
+ }
370
+ /**
371
+ * Having WON a slot, decide whether it may be kept.
372
+ *
373
+ * 🔴 THE OVER-LIMIT COUNT WAS CHECK-THEN-ACT, and it was the only thing that
374
+ * could see slots above the limit. The exclusive `wx` is the arbitration for
375
+ * `[0, limit)` and nothing else, so with two live runs on out-of-range slots
376
+ * and the limit lowered, several starts could all pass the count and then all
377
+ * win DIFFERENT in-range slots. Reproduced: five executions at a limit of
378
+ * three. The fix for one hole opened a smaller one beside it.
379
+ *
380
+ * A count cannot be made atomic, so the decision moves AFTER the exclusive
381
+ * create: take the slot, then look. At most `limit` in-range slots can exist,
382
+ * so what is left to judge is how many holders there are in total.
383
+ *
384
+ * ⚠️ THE TIE-BREAK IS THE SLOT NUMBER, so simultaneous winners do not all back
385
+ * out and leave the machine idle. A holder keeps its slot when the holders
386
+ * BELOW it, plus every out-of-range holder, still leave room; otherwise it
387
+ * gives the slot back and refuses. Deterministic, and it always leaves exactly
388
+ * as many runs as the limit allows.
389
+ */
390
+ settle(slot, limit, record) {
391
+ const others = this.allHolders(slot);
392
+ if (others.length < limit)
393
+ return { ok: true, slot };
394
+ this.releaseSlot(slot, record.runId);
395
+ return { ok: false, because: this.atCapacity(others, limit) };
396
+ }
397
+ /**
398
+ * The claim lock, if one is being held, and by whom.
399
+ *
400
+ * 🔴 A WEDGED LOCK WAS INVISIBLE TO EVERY COMMAND. It is only breakable when
401
+ * its owner is PROVEN dead, so a lock left by a process that is alive but
402
+ * hung — or by one whose pid cannot be judged — denied every start of the
403
+ * project with a message about "another start deciding" and no way to see or
404
+ * clear it. That is the same wedge the slot files had before `looply runs`
405
+ * listed them.
406
+ */
407
+ claimLock() {
408
+ const path = (0, node_path_1.join)(this.dir, 'claim.lock');
409
+ let text;
410
+ try {
411
+ text = (0, node_fs_1.readFileSync)(path, 'utf8').trim();
412
+ }
413
+ catch {
414
+ return null;
415
+ }
416
+ const pid = Number.parseInt(text, 10);
417
+ if (!Number.isInteger(pid))
418
+ return { pid: null, liveness: 'unknown' };
419
+ return { pid, liveness: this.probe(pid) };
420
+ }
421
+ /** Clear a held claim lock. A person's last resort, as `forgetSlot` is. */
422
+ forgetClaimLock() {
423
+ try {
424
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.dir, 'claim.lock'));
425
+ return true;
426
+ }
427
+ catch (e) {
428
+ if (e.code === 'ENOENT')
429
+ return false;
430
+ throw e;
431
+ }
432
+ }
433
+ /** Every slot that is still held, whatever its number. `except` is skipped. */
434
+ allHolders(except) {
435
+ const out = [];
436
+ for (const s of this.slots()) {
437
+ if (s.slot === except)
438
+ continue;
439
+ if (s.record === null)
440
+ out.push('unreadable');
441
+ else if (this.probe(s.record.pid) !== 'dead' && this.leaseAgrees(s.record))
442
+ out.push(s.record);
443
+ }
444
+ return out;
445
+ }
446
+ atCapacity(held, limit) {
447
+ /**
448
+ * 🔴 A REFUSAL THAT REPORTS FEWER HOLDERS THAN THE LIMIT IS NOT A REFUSAL A
449
+ * PERSON CAN ACT ON. The decision and this message are two observations of
450
+ * the world — the scan judged, then this walks again — and holders can
451
+ * finish in between. "already has 0 execution(s) holding a slot and the
452
+ * limit is 1" is what that looks like, and it reads as a bug in looply
453
+ * rather than as a race that will resolve itself.
454
+ */
455
+ if (held.length < limit) {
456
+ return ('every slot was taken while this run was deciding, and they have since ' +
457
+ 'been given back. Nothing is wrong — start it again');
458
+ }
459
+ const unreadable = held.filter((h) => h === 'unreadable').length;
460
+ const names = held
461
+ .filter((h) => h !== 'unreadable')
462
+ .map((r) => `${r.runId} in ${(0, node_path_1.basename)(r.workspace)}`)
463
+ .join(', ');
464
+ return (`this project already has ${held.length} execution(s) holding a slot and the limit is ${limit}` +
465
+ (names === '' ? '' : ` (${names})`) +
466
+ (unreadable === 0 ? '' : `, and ${unreadable} slot file(s) could not be read`) +
467
+ '. Raise run.max_concurrent_runs, or wait for one to finish. ' +
468
+ 'If one of those is not really running, looply runs will say so, and ' +
469
+ 'looply runs --release <runId|slot-N> clears it by name');
470
+ }
471
+ /**
472
+ * Take a free slot. The ONLY way a slot file is ever created.
473
+ *
474
+ * ⚠️ `wx` AND NOTHING ELSE. No read first, no delete first: the filesystem is
475
+ * the arbitration, exactly as it is for the run lease and the override marker.
476
+ */
477
+ take(path, record, slot) {
478
+ try {
479
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify({ ...record, slot }, null, 2)}\n`, { flag: 'wx' });
480
+ return true;
481
+ }
482
+ catch (e) {
483
+ if (LOST_THE_RACE.has(e.code ?? ''))
484
+ return false;
485
+ throw e;
486
+ }
487
+ }
488
+ /**
489
+ * Remove a slot file whose holder is proven gone — or refuse to.
490
+ *
491
+ * 🔴 THE OLD RECLAIM WAS UNLINK-THEN-CREATE, WHICH IS THE CHECK-THEN-ACT THIS
492
+ * WHOLE CLASS EXISTS TO REMOVE. It judged a slot stale, unlinked it BY PATH,
493
+ * and only then tried its exclusive create. Two starters that both saw the
494
+ * same stale slot both unlinked — and the second unlink deleted the FIRST
495
+ * one's freshly created file, so both believed they held slot 0 and both ran.
496
+ * At limit 1. Reproduced with real concurrent processes.
497
+ *
498
+ * Two things fix it, and the second is the one that actually holds:
499
+ *
500
+ * THE TOKEN `slot-<n>.reclaiming`, created with `wx`. At most one
501
+ * process reclaims a given slot at a time, so the common case
502
+ * has no race to lose.
503
+ * THE RE-READ nothing is unlinked unless it is STILL the same record this
504
+ * judged, and still gone. A slot that has been re-taken names
505
+ * a different runId, and a different runId is never removed.
506
+ *
507
+ * The re-read is what makes this correct even if the token were somehow held
508
+ * twice — after a crash, say. The token only makes the correct path the quiet
509
+ * one.
510
+ *
511
+ * ⚠️ A TOKEN LEFT BY A CRASH IS BROKEN ONLY IF ITS OWN PID IS PROVEN DEAD, and
512
+ * then the re-read still decides. Otherwise a crash during reclaim would wedge
513
+ * one slot of this project for ever.
514
+ */
515
+ reclaim(path, stale) {
516
+ const token = `${path}.reclaiming`;
517
+ if (!this.takeToken(token))
518
+ return false;
519
+ try {
520
+ if (!(0, node_fs_1.existsSync)(path))
521
+ return true;
522
+ const now = this.readSlot(path);
523
+ // Unreadable now: fail closed, exactly as the caller does.
524
+ if (now === null)
525
+ return false;
526
+ // Somebody else's slot now. Never ours to remove.
527
+ if (now.runId !== stale.runId)
528
+ return false;
529
+ if (this.probe(now.pid) !== 'dead' && this.leaseAgrees(now))
530
+ return false;
531
+ try {
532
+ (0, node_fs_1.unlinkSync)(path);
533
+ }
534
+ catch (e) {
535
+ // Gone, or held open by the process that is exiting. Either way this did
536
+ // not take the slot, and saying so is safer than throwing.
537
+ if (!LOST_THE_RACE.has(e.code ?? '') && e.code !== 'ENOENT') {
538
+ throw e;
539
+ }
540
+ if (e.code !== 'ENOENT')
541
+ return false;
542
+ }
543
+ return true;
544
+ }
545
+ finally {
546
+ // By name, as the claim lock is: a token this process does not hold is
547
+ // not this process's to remove.
548
+ this.dropToken(token);
549
+ }
550
+ }
551
+ takeToken(token) {
552
+ const write = () => {
553
+ try {
554
+ (0, node_fs_1.writeFileSync)(token, `${process.pid}\n`, { flag: 'wx' });
555
+ return true;
556
+ }
557
+ catch (e) {
558
+ if (LOST_THE_RACE.has(e.code ?? ''))
559
+ return false;
560
+ throw e;
561
+ }
562
+ };
563
+ if (write())
564
+ return true;
565
+ /**
566
+ * Held. Break it only if it cannot be DEFENDED — its owner is proven dead,
567
+ * or it names no owner at all.
568
+ *
569
+ * 🔴 THE SECOND CASE WAS MISSING, AND IT WEDGED EVERYTHING. `writeFileSync`
570
+ * creates and then writes, so a crash between the two leaves a ZERO-BYTE
571
+ * lock. `parseInt('')` is NaN, NaN is not an integer, so the lock could
572
+ * never be broken — and every `looply run` in that project waited two
573
+ * seconds and refused, for ever, over an empty file. Measured.
574
+ *
575
+ * Breaking an un-owned token is safe in both places it is used. The reclaim
576
+ * token's correctness rests on the RE-READ that follows it, not on the
577
+ * token. And the claim lock is not what makes the bound hold — `settle`
578
+ * counts the other holders AFTER an exclusive create, so a second claimant
579
+ * always sees the first; the lock stops the machine sitting IDLE when
580
+ * several claimants back out together, which is a liveness property, not a
581
+ * safety one.
582
+ */
583
+ let owner = Number.NaN;
584
+ let judged = '';
585
+ try {
586
+ judged = this.settledText(token);
587
+ const text = judged;
588
+ if (text === '') {
589
+ /**
590
+ * 🔴 AN EMPTY TOKEN IS NOT NECESSARILY ABANDONED, and the first version
591
+ * of this break assumed it was. `writeFileSync(path, data, {flag:'wx'})`
592
+ * opens exclusively and THEN writes, so a LIVE claimant's token is empty
593
+ * for the moment in between — and a second claimant that read it in that
594
+ * moment broke a lock somebody was holding.
595
+ *
596
+ * `settledText` re-reads over a short window first. A live writer fills
597
+ * it in microseconds; a crash between the two syscalls never will.
598
+ */
599
+ this.breakToken(token, '');
600
+ return write();
601
+ }
602
+ owner = Number.parseInt(text, 10);
603
+ }
604
+ catch (e) {
605
+ // Gone between the failed create and this read: try again.
606
+ if (e.code === 'ENOENT')
607
+ return write();
608
+ return false;
609
+ }
610
+ if (!Number.isInteger(owner)) {
611
+ this.breakToken(token, judged);
612
+ return write();
613
+ }
614
+ if (this.probe(owner) !== 'dead')
615
+ return false;
616
+ this.breakToken(token, judged);
617
+ return write();
618
+ }
619
+ /**
620
+ * The token's contents, once they have stopped changing.
621
+ *
622
+ * An exclusive create is two syscalls — open, then write — so a token read in
623
+ * between is empty through no fault of its holder. This re-reads over a short
624
+ * window and only reports empty if it stays empty.
625
+ */
626
+ settledText(token) {
627
+ const wait = new Int32Array(new SharedArrayBuffer(4));
628
+ for (let attempt = 0;; attempt += 1) {
629
+ const text = (0, node_fs_1.readFileSync)(token, 'utf8').trim();
630
+ if (text !== '' || attempt >= 5)
631
+ return text;
632
+ Atomics.wait(wait, 0, 0, 10);
633
+ }
634
+ }
635
+ /** Give back a token THIS process holds. One held by anybody else is left. */
636
+ dropToken(token) {
637
+ let text;
638
+ try {
639
+ text = (0, node_fs_1.readFileSync)(token, 'utf8').trim();
640
+ }
641
+ catch {
642
+ // Gone, or unreadable. Either way not ours to remove on a guess.
643
+ return;
644
+ }
645
+ if (Number.parseInt(text, 10) !== process.pid)
646
+ return;
647
+ // Pass what was read, so `breakToken` can confirm nothing changed under it.
648
+ this.breakToken(token, text);
649
+ }
650
+ /**
651
+ * Remove a token judged un-defendable — and ONLY if it is still the one that
652
+ * was judged.
653
+ *
654
+ * 🔴 THIS UNLINKED BY PATH. Two processes could both judge the same stale lock
655
+ * breakable; the first broke it and won the exclusive create, and the second —
656
+ * still inside its own break — then unlinked the FIRST one's fresh lock and
657
+ * created its own. Both were inside the critical section, and the loss
658
+ * cascaded from there. An adversarial pass measured up to four at once.
659
+ *
660
+ * `judged` is the content this decided about. If the file no longer says that,
661
+ * somebody else has already acted and this must not.
662
+ */
663
+ breakToken(token, judged) {
664
+ try {
665
+ if ((0, node_fs_1.readFileSync)(token, 'utf8').trim() !== judged)
666
+ return;
667
+ }
668
+ catch {
669
+ return; // gone already, or unreadable: not ours to remove on a guess
670
+ }
671
+ try {
672
+ (0, node_fs_1.unlinkSync)(token);
673
+ }
674
+ catch {
675
+ /* somebody broke it first */
676
+ }
677
+ }
678
+ /**
679
+ * Give back the slot THIS run holds. A slot held by anybody else is left.
680
+ *
681
+ * 🔴 IT USED TO UNLINK BY INDEX, unconditionally, and that was the worst
682
+ * defect in this file. `releaseHold` gives the lease back BEFORE the slot, and
683
+ * `claim` treats a slot whose holder's lease is gone as free — so between
684
+ * those two lines another run could legitimately reclaim slot N, and this
685
+ * run's teardown would then delete the NEW holder's claim. The bound is
686
+ * exceeded, and it stays exceeded: the same window reopens at every teardown.
687
+ *
688
+ * Every sibling here is keyed on identity — `release(runId)`, `forget(runId)`.
689
+ * This one is now too. Absent is success, and so is a slot that has moved on.
690
+ */
691
+ releaseSlot(slot, runId) {
692
+ const path = this.slotPath(slot);
693
+ const held = this.readSlot(path);
694
+ if (held === null) {
695
+ // Absent is success. Unreadable is NOT ours to delete: it may be a live
696
+ // claim caught mid-write, and this is the one place that could delete it.
697
+ return;
698
+ }
699
+ if (held.runId !== runId)
700
+ return;
701
+ try {
702
+ (0, node_fs_1.unlinkSync)(path);
703
+ }
704
+ catch (e) {
705
+ if (e.code !== 'ENOENT')
706
+ throw e;
707
+ }
708
+ }
709
+ /** Every slot, as a person needs to see it — including ones nothing can read. */
710
+ slots() {
711
+ let names;
712
+ try {
713
+ names = (0, node_fs_1.readdirSync)(this.dir).filter((n) => n.startsWith('slot-') && n.endsWith('.json'));
714
+ }
715
+ catch {
716
+ return [];
717
+ }
718
+ return (names
719
+ .map((n) => ({
720
+ slot: Number.parseInt(n.slice('slot-'.length), 10),
721
+ record: this.readSlot((0, node_path_1.join)(this.dir, n)),
722
+ path: (0, node_path_1.join)(this.dir, n),
723
+ }))
724
+ /**
725
+ * ⚠️ NUMBERED SLOTS ONLY. `slot-abc.json` parses to `NaN`, and NaN loses
726
+ * every comparison: `NaN >= limit` is false, so such a file would be
727
+ * invisible to the bound's over-limit count while still sitting in the
728
+ * directory. A slot nothing can name is not a slot — but it is still a
729
+ * file, so it is reported by `strayFiles()` below and printed by
730
+ * `looply runs`, rather than silently counted or silently ignored.
731
+ */
732
+ /**
733
+ * ⚠️ AND THE NUMBER MUST ROUND-TRIP THROUGH THE FILENAME. `parseInt` is
734
+ * lenient: `slot-01.json` parses to 1, and `forgetSlot(1)` then deletes
735
+ * `slot-1.json` — a different file, or none. A slot a person is shown
736
+ * and cannot clear is worse than one that is never shown.
737
+ */
738
+ .filter((s) => Number.isInteger(s.slot) && s.slot >= 0)
739
+ .filter((s) => (0, node_path_1.basename)(s.path) === `slot-${s.slot}.json`)
740
+ .sort((a, b) => a.slot - b.slot));
741
+ }
742
+ /**
743
+ * The files in the slots directory that `slots()` refuses to name.
744
+ *
745
+ * 🔴 THE DOCBLOCK ABOVE CLAIMED THESE WERE REPORTED, AND THEY WERE NOT.
746
+ *
747
+ * Filtering `slot-abc.json` and `slot-01.json` out of `slots()` is right —
748
+ * a slot nobody can name is not a slot, and showing a person a row that
749
+ * `--release slot-01` cannot clear is worse than showing nothing. But the
750
+ * consequence of "not a slot" was "not anywhere": no row, no count, no
751
+ * mention, while the file sat in the directory looking exactly like the
752
+ * others. That is the failure mode this whole command exists to prevent.
753
+ *
754
+ * ⚠️ IT IS INERT, NOT DANGEROUS. Nothing counts it toward the bound and
755
+ * nothing reads it. It is reported because a file a person can see on disk
756
+ * and cannot see in `looply runs` is the thing that makes people distrust
757
+ * the listing — and because `rm` is the fix, which they can only reach if
758
+ * they are told the path.
759
+ */
760
+ strayFiles() {
761
+ let names;
762
+ try {
763
+ names = (0, node_fs_1.readdirSync)(this.dir).filter((n) => n.startsWith('slot-') && n.endsWith('.json'));
764
+ }
765
+ catch {
766
+ return [];
767
+ }
768
+ const named = new Set(this.slots().map((s) => (0, node_path_1.basename)(s.path)));
769
+ return names
770
+ .filter((n) => !named.has(n))
771
+ .sort()
772
+ .map((n) => (0, node_path_1.join)(this.dir, n));
773
+ }
774
+ /**
775
+ * Clear one slot by NUMBER, whatever it holds or fails to hold.
776
+ *
777
+ * The companion to `forget(runId)` for the case a runId cannot answer: a slot
778
+ * file that is corrupt, or truncated, or written by a version that is gone.
779
+ * Nothing automatic reaches this either — a person types `slot-<n>`.
780
+ */
781
+ forgetSlot(slot) {
782
+ try {
783
+ (0, node_fs_1.unlinkSync)(this.slotPath(slot));
784
+ return true;
785
+ }
786
+ catch (e) {
787
+ if (e.code === 'ENOENT')
788
+ return false;
789
+ throw e;
790
+ }
791
+ }
792
+ /**
793
+ * Whether the run this slot names is still holding its own lease.
794
+ *
795
+ * ⚠️ A SECOND WITNESS, BECAUSE A PID IS A WEAK ONE. Pids are recycled, and a
796
+ * record left behind by a hard crash whose number a later process inherits
797
+ * probes as ALIVE for ever — so the slot it holds is never reclaimed and the
798
+ * machine's concurrency drops by one permanently, with nothing to point at.
799
+ *
800
+ * A live run always holds the lease in its own execution state directory: it
801
+ * is taken before the slot is claimed and given back in the same breath as
802
+ * the slot. So a slot whose run has NO lease is not a run, whatever its pid
803
+ * says — which covers the leak this cannot otherwise see, including the
804
+ * narrow one where releasing the lease succeeded and clearing the entry did
805
+ * not.
806
+ *
807
+ * ⚠️ IT DOES NOT CLOSE THE PID-RECYCLING HOLE, and saying so is the point: a
808
+ * hard crash leaves BOTH the lease and the record behind, so both witnesses
809
+ * agree and both are wrong. That residual is inherited from the lease's own
810
+ * liveness model, which documents it and deliberately has no heartbeat. What
811
+ * bounds it is that a person can always say so by name — see
812
+ * `looply runs --release`.
813
+ *
814
+ * ⚠️ AND IT FAILS TOWARDS "HELD". An unreadable lease is not evidence of
815
+ * absence; only a lease that is gone, or one that names a DIFFERENT run,
816
+ * frees the slot.
817
+ */
818
+ leaseAgrees(record) {
819
+ const path = (0, run_lease_1.leasePathIn)(record.stateDir);
820
+ let text;
821
+ try {
822
+ text = (0, node_fs_1.readFileSync)(path, 'utf8');
823
+ }
824
+ catch (e) {
825
+ // ENOENT is an answer; anything else is a failure to ask.
826
+ return e.code !== 'ENOENT';
827
+ }
828
+ try {
829
+ const lease = JSON.parse(text);
830
+ return typeof lease.runId !== 'string' || lease.runId === record.runId;
831
+ }
832
+ catch {
833
+ // Torn or half-written — a live run's lease, mid-rename, looks like this.
834
+ return true;
835
+ }
836
+ }
837
+ /**
838
+ * Forget a run entirely: its record and any slot it holds.
839
+ *
840
+ * ⚠️ THE ONLY THING THAT CAN CLEAR A SLOT WHOSE PID LOOKS ALIVE, and it exists
841
+ * for exactly one situation: a pid recycled onto a crashed run's number, where
842
+ * every automatic witness agrees and every one of them is wrong. It is not
843
+ * called by anything automatic — a person names the run.
844
+ */
845
+ forget(runId) {
846
+ safeRunId(runId);
847
+ let slot = null;
848
+ let names = [];
849
+ try {
850
+ // `.json` as well as the prefix: a reclaim token is `slot-N.json.reclaiming`,
851
+ // and it is never a slot.
852
+ names = (0, node_fs_1.readdirSync)(this.dir).filter((n) => n.startsWith('slot-') && n.endsWith('.json'));
853
+ }
854
+ catch {
855
+ names = [];
856
+ }
857
+ for (const name of names) {
858
+ const held = this.readSlot((0, node_path_1.join)(this.dir, name));
859
+ if (held?.runId !== runId)
860
+ continue;
861
+ slot = Number.parseInt(name.slice('slot-'.length), 10);
862
+ try {
863
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.dir, name));
864
+ }
865
+ catch {
866
+ /* already gone */
867
+ }
868
+ }
869
+ let forgot = true;
870
+ try {
871
+ (0, node_fs_1.unlinkSync)(this.pathFor(runId));
872
+ }
873
+ catch (e) {
874
+ if (e.code === 'ENOENT')
875
+ forgot = false;
876
+ else
877
+ throw e;
878
+ }
879
+ return { forgot, slot };
880
+ }
881
+ slotPath(slot) {
882
+ return (0, node_path_1.join)(this.dir, `slot-${slot}.json`);
883
+ }
884
+ readSlot(path) {
885
+ try {
886
+ const r = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
887
+ if (typeof r.runId !== 'string' || typeof r.pid !== 'number')
888
+ return null;
889
+ /**
890
+ * 🔴 AND THE PATH FIELDS, for the same reason `list` checks them. This
891
+ * validated `runId` and `pid` only, so a slot record reached `atCapacity`,
892
+ * which calls `basename(r.workspace)` — and `claim`, whose whole contract
893
+ * is `{ ok: false, because }`, THREW a TypeError instead of refusing. A
894
+ * bound that crashes is not a bound.
895
+ *
896
+ * `leaseAgrees` reads `stateDir` too, so both are required to be strings
897
+ * before this record is handed to anything.
898
+ */
899
+ if (typeof r.workspace !== 'string' || typeof r.stateDir !== 'string')
900
+ return null;
901
+ /**
902
+ * ⚠️ THE SAME DOORWAY RULE AS `list`. A slot file is written by another
903
+ * process and read without coordination, so its `runId` is untrusted for
904
+ * exactly the reason a record's is — and it flows on to `forget(runId)`,
905
+ * which builds a path from it and throws on one it cannot address. An
906
+ * unaddressable id makes the slot UNREADABLE, which every caller already
907
+ * handles by failing closed, rather than a value that reaches a path.
908
+ */
909
+ if (!RUN_ID.test(r.runId))
910
+ return null;
911
+ return r;
912
+ }
913
+ catch {
914
+ return null;
915
+ }
916
+ }
917
+ /** Give the slot back. Absent is success: releasing twice is not an error. */
918
+ release(runId) {
919
+ try {
920
+ (0, node_fs_1.unlinkSync)(this.pathFor(runId));
921
+ }
922
+ catch (e) {
923
+ if (e.code !== 'ENOENT')
924
+ throw e;
925
+ }
926
+ }
927
+ /** Every entry, with its process probed. Never throws on a bad file. */
928
+ list() {
929
+ let names;
930
+ try {
931
+ // Slot files are the BOUND, not run records; counting them here would
932
+ // report every run twice.
933
+ names = (0, node_fs_1.readdirSync)(this.dir).filter((n) => n.endsWith('.json') && !n.startsWith('slot-'));
934
+ }
935
+ catch {
936
+ return [];
937
+ }
938
+ const out = [];
939
+ for (const name of names) {
940
+ const path = (0, node_path_1.join)(this.dir, name);
941
+ let record;
942
+ try {
943
+ record = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
944
+ }
945
+ catch (e) {
946
+ out.push({ kind: 'unreadable', path, because: e.message });
947
+ continue;
948
+ }
949
+ if (typeof record.runId !== 'string' || typeof record.pid !== 'number') {
950
+ out.push({ kind: 'unreadable', path, because: 'not a run record' });
951
+ continue;
952
+ }
953
+ /**
954
+ * 🔴 THE runId IS VALIDATED HERE, WHERE IT ENTERS, and it was not.
955
+ *
956
+ * A registry record is a JSON file. `register` refuses a runId that is not
957
+ * a safe path segment — but `list` did not, so anything that could write
958
+ * the project's state directory could hand every consumer a runId with a
959
+ * separator in it. Measured, end to end, against the built CLI:
960
+ *
961
+ * a record naming `x/../queue`, with a live pid
962
+ * -> `looply stop` built `<state>/control.x/../queue.json`
963
+ * -> which resolves to `<state>/queue.json`
964
+ * -> and OVERWROTE THE PROJECT'S WORK QUEUE with a control request.
965
+ *
966
+ * Validating at each consumer would have been four places and a fifth one
967
+ * later. This is the doorway: a record whose id cannot be addressed is
968
+ * reported as unreadable, which every consumer already handles, and none
969
+ * of them ever sees it as a record again.
970
+ */
971
+ if (!RUN_ID.test(record.runId)) {
972
+ out.push({
973
+ kind: 'unreadable',
974
+ path,
975
+ because: `its run id ${JSON.stringify(record.runId)} is not one this can address`,
976
+ });
977
+ continue;
978
+ }
979
+ /**
980
+ * 🔴 AND THE TWO PATH FIELDS, because validating the runId alone left the
981
+ * door open beside the one it closed.
982
+ *
983
+ * A record carries `workspace` and `stateDir`, both absolute paths, both
984
+ * read from a file. `requestControlEverywhere` WRITES into `stateDir`, so
985
+ * a record naming any directory on the machine turned `looply stop` into
986
+ * a write primitive pointed wherever the record said. `reapVerdict`
987
+ * contains `workspace` by realpath before removing anything, but nothing
988
+ * contained `stateDir` before writing to it.
989
+ *
990
+ * Every record looply writes has `stateDir === <workspace>/.looply/state`,
991
+ * because that is how `executionStateDir` builds it. A record that says
992
+ * otherwise did not come from looply, and is unreadable.
993
+ */
994
+ if (typeof record.workspace !== 'string' || typeof record.stateDir !== 'string') {
995
+ out.push({ kind: 'unreadable', path, because: 'its workspace or state directory is missing' });
996
+ continue;
997
+ }
998
+ if ((0, node_path_1.resolve)(record.stateDir) !== (0, node_path_1.resolve)((0, node_path_1.join)(record.workspace, git_driver_1.STATE_DIR, 'state'))) {
999
+ out.push({
1000
+ kind: 'unreadable',
1001
+ path,
1002
+ because: 'its state directory is not the one its workspace would have',
1003
+ });
1004
+ continue;
1005
+ }
1006
+ const liveness = this.probe(record.pid);
1007
+ if (liveness === 'alive')
1008
+ out.push({ kind: 'active', record });
1009
+ else if (liveness === 'dead')
1010
+ out.push({ kind: 'orphaned', record });
1011
+ else
1012
+ out.push({ kind: 'unknown', record });
1013
+ }
1014
+ return out;
1015
+ }
1016
+ /**
1017
+ * Everything NOT PROVEN DEAD — what capacity counts and what `stop` signals.
1018
+ *
1019
+ * ⚠️ 'unknown' IS INCLUDED, DELIBERATELY. Both callers are safer over-counting
1020
+ * than under-counting: capacity would otherwise start an extra execution, and
1021
+ * `stop` would otherwise report a run as finished while it is still writing.
1022
+ */
1023
+ active() {
1024
+ return this.list().flatMap((e) => (e.kind === 'active' || e.kind === 'unknown' ? [e.record] : []));
1025
+ }
1026
+ }
1027
+ exports.RunRegistry = RunRegistry;
1028
+ /**
1029
+ * 🔴 `capacityVerdict` WAS DELETED, and its absence is the point.
1030
+ *
1031
+ * It counted live records and judged them against the limit, and it read like
1032
+ * the concurrency bound — but `claim` replaced it and nothing called it any
1033
+ * more. A function that looks like the bound, is exported, is tested, and
1034
+ * decides nothing is worse than no function at all: the next person to reach
1035
+ * for it gets check-then-act semantics back, with tests that pass.
1036
+ *
1037
+ * The bound is `RunRegistry.claim`. There is one.
1038
+ */
1039
+ /**
1040
+ * Write a control request where EVERY live execution will read it.
1041
+ *
1042
+ * 🔴 ONE SHARED FILE PLUS N CONSUMERS IS A BRAKE THAT RELEASES ITSELF, and both
1043
+ * writers had the same hole. `ControlStore.consume()` writes `resume` into the
1044
+ * file it read, so the FIRST execution to reach a round boundary clears the
1045
+ * instruction and every other one reads `resume` and carries on.
1046
+ *
1047
+ * `looply stop` was taught to write an addressed request per execution; the
1048
+ * DAEMON — which is how the phone's Stop and Pause arrive — was not, and it is
1049
+ * the path that matters most, because nobody is standing at the terminal to
1050
+ * notice that two of three runs kept going.
1051
+ *
1052
+ * So there is one function, and both call it.
1053
+ *
1054
+ * ⚠️ THE SHARED FILE IS STILL WRITTEN, FIRST. It is what a run started AFTER
1055
+ * this request will read, what Simple Mode has always used, and what `resume`
1056
+ * has to clear. The addressed files are additive.
1057
+ *
1058
+ * ⚠️ AND A FAILURE TO REACH ONE EXECUTION IS REPORTED, not swallowed. A brake
1059
+ * that misses a run must say which.
1060
+ */
1061
+ async function requestControlEverywhere(projectState, live, kind, actor, fs,
1062
+ /** Where this project's executions may legitimately keep state. See below. */
1063
+ cwd) {
1064
+ await new control_file_1.ControlStore(projectState, fs).request(kind, actor);
1065
+ /**
1066
+ * 🔴 AND IT WRITES ONLY WHERE A RUN'S STATE CAN LEGITIMATELY BE.
1067
+ *
1068
+ * `list` refuses a record whose `stateDir` is not its workspace's — but
1069
+ * `workspace` itself was unconstrained, so a record naming
1070
+ * `C:\anywhere` with `stateDir` `C:\anywhere\.looply\state` passed the
1071
+ * doorway, and `ControlStore.request` then MKDIR'd and WROTE there. `looply
1072
+ * stop` and the phone's Stop were still a write primitive, one level further
1073
+ * out than the runId hole they were supposed to have closed.
1074
+ *
1075
+ * There are exactly two places an execution's state can be: the project's own
1076
+ * state directory (an ordinary checkout, where project and execution scope are
1077
+ * the same path), or under this project's workspaces root. Anything else is
1078
+ * not this project's execution, whatever the record says.
1079
+ */
1080
+ /**
1081
+ * 🔴 AND THE RULE IS "A CHECKOUT OF THIS REPOSITORY", NOT "ONE OF TWO PLACES".
1082
+ *
1083
+ * A first version allowed only the project's own state directory or something
1084
+ * under the workspaces root — and that broke the phase's own `--in` workflow,
1085
+ * where a person points a run at a worktree THEY made, which is neither.
1086
+ * Measured: `looply stop` reported a live `--in` execution as unreachable and
1087
+ * wrote nothing, which is the silent-brake failure this whole mechanism exists
1088
+ * to end.
1089
+ *
1090
+ * The honest question is not "where is this directory" but "is it a checkout
1091
+ * of the repository I am the registry for". `list` already guarantees the
1092
+ * state directory is its workspace's; this settles the workspace.
1093
+ */
1094
+ const mine = cwd === undefined ? null : (0, trusted_git_1.repoIdentityPath)(cwd);
1095
+ const reached = [];
1096
+ const unreachable = [];
1097
+ for (const r of live) {
1098
+ /**
1099
+ * ⚠️ A CHECKOUT *ROOT* OF THIS REPOSITORY, not merely somewhere inside it.
1100
+ * `repoIdentityPath` walks up, so `<repo>/src` answers `<repo>` and matched
1101
+ * — and this would then have created `<repo>/src/.looply/state` and written
1102
+ * into it. Confined to the person's own repository rather than the whole
1103
+ * machine, but still a write nobody asked for, in a tree looply is supposed
1104
+ * to leave alone unless a round is committing.
1105
+ */
1106
+ if (mine !== null && (!(0, trusted_git_1.isCheckoutRoot)(r.workspace) || (0, trusted_git_1.repoIdentityPath)(r.workspace) !== mine)) {
1107
+ unreachable.push(r.runId);
1108
+ continue;
1109
+ }
1110
+ try {
1111
+ await control_file_1.ControlStore.addressed(r.stateDir, r.runId, fs).request(kind, actor);
1112
+ reached.push(r.runId);
1113
+ }
1114
+ catch {
1115
+ // A workspace that has gone, or one this user cannot write.
1116
+ unreachable.push(r.runId);
1117
+ }
1118
+ }
1119
+ return { reached, unreachable };
1120
+ }
1121
+ /**
1122
+ * Create a workspace for an execution: a linked worktree on a fresh branch.
1123
+ *
1124
+ * ⚠️ THE GIT DRIVER DOES THE WORK, so the worktree is created by the same
1125
+ * substrate every other verb uses — the repository-config refusal, the
1126
+ * repository-root check, the rebuilt `GIT_` environment and the absolute git
1127
+ * binary all apply. A provisioner with its own runner would be a second path
1128
+ * around four defences at once.
1129
+ */
1130
+ async function createWorkspace(git, cwd, runId, branch, baseSha,
1131
+ /** Whose home the workspaces root lives under; the user's unless a test says otherwise (M4-1). */
1132
+ home = (0, node_os_1.homedir)(),
1133
+ /**
1134
+ * `run.carry_into_checkouts` — directories this fresh checkout needs that git
1135
+ * does not carry, above all `node_modules`.
1136
+ *
1137
+ * 🔴 WITHOUT IT THE GATE CANNOT PASS HERE. A worktree holds what git tracks,
1138
+ * the gate runs with no network by design, and so a second worker's every
1139
+ * round went red on the same missing toolchain. Measured: four rounds, one
1140
+ * output digest.
1141
+ *
1142
+ * ⚠️ LAST AND DEFAULTED, so every existing caller and fixture is untouched and
1143
+ * an empty list is byte-identical to the old behaviour.
1144
+ */
1145
+ carry = []) {
1146
+ const root = workspacesRoot(cwd, home);
1147
+ (0, node_fs_1.mkdirSync)(root, { recursive: true });
1148
+ const path = (0, node_path_1.join)(root, runId);
1149
+ if ((0, node_fs_1.existsSync)(path)) {
1150
+ throw new WorkspaceError(`a workspace already exists at ${path} — refusing to reuse it`);
1151
+ }
1152
+ await git.addWorktree(path, branch, baseSha);
1153
+ /**
1154
+ * Immediately after the checkout and BEFORE the claim below, so a workspace is
1155
+ * never offered to a run in a state where its gate is guaranteed to fail.
1156
+ */
1157
+ if (carry.length > 0) {
1158
+ try {
1159
+ (0, checkout_carry_1.carryIntoCheckout)(cwd, path, carry);
1160
+ }
1161
+ catch (e) {
1162
+ throw new WorkspaceError(`the workspace could not be provisioned: ${e.message}`);
1163
+ }
1164
+ }
1165
+ /**
1166
+ * ⚠️ CLAIMED THE INSTANT IT EXISTS. A run does not take its lease until after
1167
+ * the config is read and the git checks have run — seconds later — and until
1168
+ * then nothing in the checkout says anybody is coming. `abandonedWorkspaces`
1169
+ * would call it abandoned and a concurrent `--reap` would delete it.
1170
+ */
1171
+ try {
1172
+ const stateDir = (0, node_path_1.join)(path, git_driver_1.STATE_DIR, 'state');
1173
+ (0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
1174
+ /**
1175
+ * ⚠️ TEMP-THEN-RENAME, so a reader can never catch it half-written. That
1176
+ * matters because `checkoutBusy` has to decide what an unparseable marker
1177
+ * means: written atomically, a torn one cannot exist, so a corrupt one is
1178
+ * corruption rather than a race and can be read as stale.
1179
+ */
1180
+ const tmp = (0, node_path_1.join)(stateDir, `${CREATING}.${process.pid}.tmp`);
1181
+ (0, node_fs_1.writeFileSync)(tmp, `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() }, null, 2)}
1182
+ `);
1183
+ (0, node_fs_1.renameSync)(tmp, (0, node_path_1.join)(stateDir, CREATING));
1184
+ }
1185
+ catch (e) {
1186
+ /**
1187
+ * 🔴 THIS USED TO BE SWALLOWED, and swallowing it produced exactly the state
1188
+ * the marker exists to prevent.
1189
+ *
1190
+ * The marker is the ONLY thing covering the checkout between `worktree add`
1191
+ * returning and the run acquiring its lease — no registry record exists yet,
1192
+ * so `abandonedWorkspaces` has nothing else to go on. A `try {} catch {}`
1193
+ * around the write meant a refused mkdir, write or rename produced an
1194
+ * unprotected checkout with no marker and no diagnostic: a concurrent
1195
+ * `--reap` would remove it out from under a starting run, and nothing
1196
+ * anywhere would say why.
1197
+ *
1198
+ * So it fails closed: the worktree this just created is removed and the
1199
+ * caller is told. A workspace that cannot be protected is not a workspace.
1200
+ */
1201
+ try {
1202
+ await git.removeWorktree(path);
1203
+ await git.pruneWorktrees();
1204
+ }
1205
+ catch {
1206
+ // Leave it rather than force it. It is empty and `looply runs` lists it.
1207
+ }
1208
+ throw new WorkspaceError(`the workspace at ${path} could not be claimed for this run (${e.message}), so it was not kept`);
1209
+ }
1210
+ return path;
1211
+ }
1212
+ /**
1213
+ * Whether a workspace may be reaped, and why not when it may not.
1214
+ *
1215
+ * 🔴 THE ORDER OF THESE CHECKS IS THE SAFETY. A reaper that deleted first and
1216
+ * asked afterwards is a reaper that deletes a running execution's checkout. So:
1217
+ * it must be registered as orphaned (its process gone), it must be a workspace
1218
+ * this machine created under the workspaces root, and it must not be the main
1219
+ * checkout. Anything else is left alone and reported.
1220
+ */
1221
+ /**
1222
+ * The real path of `path`, resolving as much of it as actually exists.
1223
+ *
1224
+ * ⚠️ A PLAIN `realpathSync` IS NOT ENOUGH HERE, and the first version was one:
1225
+ * it throws ENOENT for anything absent, which is the ordinary case twice over —
1226
+ * the workspaces root before any workspace has been created, and a workspace
1227
+ * whose directory is already gone. Refusing those as "could not be resolved"
1228
+ * made the reaper unable to clear a registry entry for a directory that no
1229
+ * longer exists, which is precisely what it is for.
1230
+ *
1231
+ * So: resolve the longest EXISTING prefix, then re-append the rest. Links in the
1232
+ * part that exists are still collapsed — which is the whole point — and the
1233
+ * absent tail contributes no link to follow.
1234
+ */
1235
+ function realpathDeep(path) {
1236
+ const full = (0, node_path_1.resolve)(path);
1237
+ const tail = [];
1238
+ let head = full;
1239
+ for (;;) {
1240
+ try {
1241
+ const real = node_fs_1.realpathSync.native(head);
1242
+ return tail.length === 0 ? real : (0, node_path_1.join)(real, ...tail);
1243
+ }
1244
+ catch {
1245
+ const parent = (0, node_path_1.dirname)(head);
1246
+ if (parent === head)
1247
+ return full; // reached the drive/root; nothing resolved
1248
+ tail.unshift((0, node_path_1.basename)(head));
1249
+ head = parent;
1250
+ }
1251
+ }
1252
+ }
1253
+ /**
1254
+ * Whether `child` is strictly inside `parent`.
1255
+ *
1256
+ * ⚠️ THE SEPARATOR IS PART OF THE TEST. A bare `startsWith` says
1257
+ * `C:\ws-evil` is inside `C:\ws`, and `relative()` alone says a sibling is
1258
+ * inside when it isn't. Equality is excluded too: the root itself is never a
1259
+ * workspace.
1260
+ */
1261
+ function contains(parent, child) {
1262
+ const rel = (0, node_path_1.relative)(parent, child);
1263
+ return rel !== '' && !rel.startsWith('..') && !(0, node_path_1.isAbsolute)(rel);
1264
+ }
1265
+ /**
1266
+ * WHY THE REAPER IS KEEPING THIS CHECKOUT, IN WORDS THAT ARE TRUE.
1267
+ *
1268
+ * 🔴 THE OLD SENTENCE WAS WRONG IN THREE WAYS AT ONCE WHEN THE CAUSE WAS AN
1269
+ * UNINSPECTABLE CHECKOUT — and that is the one case that writes a durable
1270
+ * quarantine, so it was wrong exactly where it mattered most. Measured against
1271
+ * an empty, clean worktree during a transient git fault, it said:
1272
+ *
1273
+ * "it still holds 1 file(s) nothing else has a copy of (could not be
1274
+ * inspected (…)). Look at <path>, then remove it yourself"
1275
+ *
1276
+ * 1. It claimed a file count for a checkout holding nothing. The "1 file" was
1277
+ * the error string itself.
1278
+ * 2. It never said a quarantine had been written, and never named `--release`
1279
+ * — that guidance appeared only on the NEXT invocation.
1280
+ * 3. It told the person to hand-delete a worktree looply had just decided
1281
+ * must not be touched.
1282
+ */
1283
+ function keptBecause(residue, path, quarantined) {
1284
+ const blind = residue.find((r) => r.startsWith('could not be inspected'));
1285
+ if (blind !== undefined) {
1286
+ return (`looply could not see inside it, so it cannot say whether anything there is yours — ${blind}. ` +
1287
+ (quarantined
1288
+ ? `It is quarantined and will not be touched. Look at ${path}, then ` +
1289
+ `\`looply runs --release ${(0, node_path_1.basename)(path)}\` when you have`
1290
+ : `Look at ${path} yourself; nothing here will remove it`));
1291
+ }
1292
+ return (`it still holds ${residue.length} file(s) nothing else has a copy of ` +
1293
+ `(${residue.slice(0, 3).join(', ')}${residue.length > 3 ? ', …' : ''}). ` +
1294
+ `Look at ${path}, then remove it yourself`);
1295
+ }
1296
+ /**
1297
+ * 🔴 THE PART OF A REAP REFUSAL THAT NEVER CHANGES, ASKED ON ITS OWN.
1298
+ *
1299
+ * `looply runs` counted the orphans `--reap` could take by `record.worktree`,
1300
+ * which does not mean "a checkout looply made". It means "not the main
1301
+ * checkout" — and it is TRUE for a worktree a PERSON made and pointed `--in`
1302
+ * at. So a crashed `--in` run was announced as "in a checkout looply made",
1303
+ * the person ran `--reap`, and `--reap` refused it. Correctly, permanently,
1304
+ * and once a day for ever.
1305
+ *
1306
+ * ⚠️ LIVENESS IS DELIBERATELY NOT ASKED HERE. That answer changes between the
1307
+ * listing and the reap; this one does not. `reapVerdict` calls this, so the
1308
+ * count in the summary and the refusal that follows it cannot drift apart
1309
+ * again — there is one test, not two spellings of nearly the same one.
1310
+ */
1311
+ function reapableCheckout(record, cwd,
1312
+ /** Whose home the workspaces root lives under; the user's unless a caller says otherwise (M4-4). */
1313
+ home = (0, node_os_1.homedir)()) {
1314
+ if (!record.worktree) {
1315
+ return { ok: false, because: 'this is the main checkout, which no execution created' };
1316
+ }
1317
+ /**
1318
+ * 🔴 CONTAINMENT IS CHECKED ON THE *REAL* PATH, because `resolve` does not
1319
+ * follow links and Windows junctions are trivial to make without privileges.
1320
+ *
1321
+ * The attack the first version allowed, in full: create
1322
+ * `~/.looply/workspaces/<key>/run_x` as a JUNCTION pointing at a live
1323
+ * sibling's checkout — or at the user's repository. `resolve` returns the
1324
+ * junction's own path, `startsWith(root)` says "inside the workspaces root",
1325
+ * and the reaper then runs `worktree remove` and `rm` through it, deleting
1326
+ * somebody else's work at the far end. Nothing about the record has to be
1327
+ * forged; only a directory has to be planted, and the record can be an
1328
+ * ordinary orphan of a genuinely dead run.
1329
+ *
1330
+ * `realpathSync` collapses junctions, symlinks and 8.3 short names, so both
1331
+ * sides are compared as the filesystem sees them. The root is realpathed too:
1332
+ * on macOS `/var` is a link to `/private/var`, and comparing a resolved child
1333
+ * against an unresolved parent would refuse every legitimate reap there.
1334
+ */
1335
+ const rootReal = realpathDeep(workspacesRoot(cwd, home));
1336
+ const declared = (0, node_path_1.resolve)(record.workspace);
1337
+ const ws = realpathDeep(declared);
1338
+ if (!contains(rootReal, ws)) {
1339
+ return {
1340
+ ok: false,
1341
+ because: (ws === declared
1342
+ ? `${declared} is not under this project's workspaces root`
1343
+ : `${declared} really lives at ${ws}, which is outside this project's workspaces root. ` +
1344
+ 'A workspace that is a link to somewhere else is not a workspace this may remove'),
1345
+ };
1346
+ }
1347
+ return { ok: true };
1348
+ }
1349
+ function reapVerdict(record, cwd, probe = exports.nodeProbe, home = (0, node_os_1.homedir)()) {
1350
+ const liveness = probe(record.pid);
1351
+ if (liveness !== 'dead') {
1352
+ return {
1353
+ ok: false,
1354
+ because: liveness === 'alive'
1355
+ ? `pid ${record.pid} is still alive`
1356
+ : `pid ${record.pid} could not be judged alive or dead, and a reaper does not guess`,
1357
+ };
1358
+ }
1359
+ const ours = reapableCheckout(record, cwd, home);
1360
+ if (!ours.ok)
1361
+ return ours;
1362
+ const declared = (0, node_path_1.resolve)(record.workspace);
1363
+ // Gone already: nothing to remove, but the registry entry is still ours to clear.
1364
+ if (!(0, node_fs_1.existsSync)(declared))
1365
+ return { ok: true };
1366
+ /**
1367
+ * 🔴 A DEAD RECORD DOES NOT MEAN AN IDLE CHECKOUT, and this only ever asked
1368
+ * about the record.
1369
+ *
1370
+ * A crashed run leaves its registry entry naming workspace W. Somebody then
1371
+ * starts a NEW run in W — `looply run --in W` — which is allowed, because the
1372
+ * crashed run's lease is judged dead and taken over. Now W holds a live
1373
+ * execution AND a stale record for a dead pid, and `--reap` would prove the
1374
+ * STALE pid dead and delete the live run's checkout out from under it, filing
1375
+ * its lease and ledger under the dead run's name.
1376
+ *
1377
+ * The workspace's own lease is the answer, exactly as it is for a slot: if
1378
+ * anything in there is holding one, this is not a dead workspace whatever the
1379
+ * record says.
1380
+ */
1381
+ if (checkoutBusy((0, node_path_1.join)(declared, git_driver_1.STATE_DIR, 'state'), probe)) {
1382
+ return { ok: false, because: `something is still using that checkout (${busyBecause((0, node_path_1.join)(declared, git_driver_1.STATE_DIR, 'state'), probe)})` };
1383
+ }
1384
+ try {
1385
+ // `lstat`, not `stat`: a link TO a directory is not a directory this owns.
1386
+ if (!(0, node_fs_1.lstatSync)(declared).isDirectory()) {
1387
+ return { ok: false, because: `${declared} is not a directory` };
1388
+ }
1389
+ }
1390
+ catch (e) {
1391
+ return { ok: false, because: `${declared} could not be read (${e.message})` };
1392
+ }
1393
+ return { ok: true };
1394
+ }
1395
+ /**
1396
+ * Remove a proven-dead workspace, worktree record and registry entry.
1397
+ *
1398
+ * ⚠️ `git worktree remove` WITHOUT `--force`, then nothing. If git refuses —
1399
+ * because the tree is dirty, or a file is held — the directory is LEFT and the
1400
+ * refusal is returned. `--force` deletes ignored files, which is how a reaper
1401
+ * would take somebody's `node_modules` or `.env` with it, and it fires exactly
1402
+ * when git has already judged there is something worth refusing over.
1403
+ */
1404
+ /**
1405
+ * Everything in a workspace that is not a pristine checkout — INCLUDING ignored
1406
+ * files, which is the whole reason this exists.
1407
+ *
1408
+ * ⚠️ THE DRIVER MUST BE BUILT FOR THE WORKSPACE, not for the main checkout.
1409
+ * `trustedGitEnv` pins GIT_DIR and GIT_WORK_TREE from the cwd a driver was made
1410
+ * with, and a pinned work tree beats `-C` — so a main-checkout driver asked
1411
+ * about a workspace answers about the main checkout, confidently and wrongly.
1412
+ *
1413
+ * A failure to ask counts AS residue: a reaper that cannot see what is there
1414
+ * must not delete it.
1415
+ */
1416
+ async function workspaceResidue(gitInWorkspace, workspace,
1417
+ /**
1418
+ * `run.carry_into_checkouts` — paths LOOPLY PUT HERE ITSELF, and therefore not
1419
+ * the person's.
1420
+ *
1421
+ * 🔴 WITHOUT THIS, PROVISIONING A WORKSPACE MAKES IT UNREAPABLE FOR EVER.
1422
+ * `--ignored` is the whole point of this check, and a carried `node_modules`
1423
+ * is ignored by definition — so the reaper counted the product's own copy as
1424
+ * "file(s) nothing else has a copy of" and refused. Measured immediately after
1425
+ * the carry landed: SIX workspaces, every one kept, each naming
1426
+ * `!! node_modules/` as the person's content.
1427
+ *
1428
+ * The reasoning is the same one that drops `.looply/state/` two lines below:
1429
+ * this is looply's own, it was copied from a source that still has it, and
1430
+ * deleting it loses nothing. It is a COPY — that is the whole contract of
1431
+ * `carryIntoCheckout` — so "nothing else has a copy of it" is exactly false.
1432
+ *
1433
+ * ⚠️ TOP-LEVEL NAMES ONLY, matching what `carryNameTrouble` permits. A path
1434
+ * deeper than one segment is not carriable, so it cannot be excused here.
1435
+ */
1436
+ carried = []) {
1437
+ let lines;
1438
+ try {
1439
+ lines = (await gitInWorkspace.statusWithIgnored())
1440
+ .split('\n')
1441
+ .map((l) => l.trim())
1442
+ .filter((l) => l !== '');
1443
+ }
1444
+ catch (e) {
1445
+ return [`could not be inspected (${e.message})`];
1446
+ }
1447
+ const out = [];
1448
+ for (const line of lines) {
1449
+ /**
1450
+ * Porcelain v1 is `XY PATH` or `XY ORIG -> PATH`. Only the path field is
1451
+ * examined: a looser match would filter `R secrets.env -> .looply/x` — a
1452
+ * USER file, renamed — by the very rule meant to ignore looply's own state.
1453
+ */
1454
+ const field = line.slice(3).trim();
1455
+ const shown = field.startsWith('"') ? field.slice(1) : field;
1456
+ // Looply's own execution state, which `retainEvidence` moves to safety.
1457
+ if (shown.startsWith('.looply/state/') || shown.startsWith('.looply\\state\\'))
1458
+ continue;
1459
+ /**
1460
+ * Looply's own provisioning. Git collapses an ignored directory to one line,
1461
+ * so a carried `node_modules` arrives as `!! node_modules/` — matched here
1462
+ * with and without the trailing separator, in both slash spellings.
1463
+ */
1464
+ if (carried.some((name) => shown === name || shown === `${name}/` || shown === `${name}\\`))
1465
+ continue;
1466
+ /**
1467
+ * 🔴 GIT COLLAPSES AN IGNORED DIRECTORY TO ONE LINE, and a project that
1468
+ * ignores `.looply/` wholesale — which many do — gets `!! .looply/` and
1469
+ * nothing else, whatever is inside it. Neither `-uall` nor a pathspec
1470
+ * exclusion expands it: `--ignored=matching` reports the directory because
1471
+ * the directory is what the ignore rule matched. Measured.
1472
+ *
1473
+ * So the filter stopped matching, every such workspace read as holding
1474
+ * residue for ever, and `--reap` refused it with a reason about the
1475
+ * person's files that named none of them.
1476
+ *
1477
+ * When git will not say what is in there, LOOK. Anything outside `state/`
1478
+ * is the person's and is named individually; if there is nothing else, this
1479
+ * line is looply's own and is dropped.
1480
+ */
1481
+ if (shown === '.looply/' || shown === '.looply' || shown === '.looply\\') {
1482
+ out.push(...notOursUnder((0, node_path_1.join)(workspace, git_driver_1.STATE_DIR)));
1483
+ continue;
1484
+ }
1485
+ out.push(line);
1486
+ }
1487
+ return out;
1488
+ }
1489
+ /**
1490
+ * Everything under `<workspace>/.looply` that is not looply's own `state/`.
1491
+ *
1492
+ * ⚠️ A FAILURE TO LOOK IS RESIDUE. If the directory cannot be read, the honest
1493
+ * answer is "there may be something here", not "there is nothing".
1494
+ */
1495
+ function notOursUnder(looplyDir) {
1496
+ const found = [];
1497
+ const walk = (dir, rel) => {
1498
+ let entries;
1499
+ try {
1500
+ entries = (0, node_fs_1.readdirSync)(dir);
1501
+ }
1502
+ catch (e) {
1503
+ found.push(`${rel || '.looply'} could not be read (${e.message})`);
1504
+ return;
1505
+ }
1506
+ for (const name of entries) {
1507
+ const at = rel === '' ? name : `${rel}/${name}`;
1508
+ if (rel === '' && name === 'state')
1509
+ continue; // looply's own, moved to safety
1510
+ const full = (0, node_path_1.join)(dir, name);
1511
+ let isDir = false;
1512
+ try {
1513
+ isDir = (0, node_fs_1.lstatSync)(full).isDirectory();
1514
+ }
1515
+ catch {
1516
+ found.push(`.looply/${at} could not be read`);
1517
+ continue;
1518
+ }
1519
+ if (isDir)
1520
+ walk(full, at);
1521
+ else
1522
+ found.push(`?? .looply/${at}`);
1523
+ }
1524
+ };
1525
+ walk(looplyDir, '');
1526
+ return found;
1527
+ }
1528
+ /**
1529
+ * Move a dead execution's own state out of the workspace, so removing the
1530
+ * checkout does not take the evidence with it.
1531
+ *
1532
+ * ⚠️ MOVED, NEVER DELETED, and the reap stops if this fails. A crashed run's
1533
+ * ledger is the only account of why it crashed; a reaper that removes the
1534
+ * checkout and loses it has destroyed the one thing somebody would come looking
1535
+ * for. It lands in the PROJECT's state directory, which outlives every workspace.
1536
+ *
1537
+ * ⚠️ AND IT NEVER OVERWRITES. A second reap of the same runId — which can only
1538
+ * happen if the first was interrupted — lands beside the first, not on it.
1539
+ */
1540
+ function retainEvidence(record, cwd) {
1541
+ const from = (0, node_path_1.join)(record.workspace, git_driver_1.STATE_DIR, 'state');
1542
+ if (!(0, node_fs_1.existsSync)(from))
1543
+ return { ok: true, at: null };
1544
+ /**
1545
+ * 🔴 AND IT MUST REALLY BE INSIDE THE WORKSPACE. `reapVerdict` realpaths the
1546
+ * workspace ROOT; this then built a path two levels below it and ran a
1547
+ * recursive copy AND a recursive delete through it. A junction at
1548
+ * `<workspace>/.looply` — which nothing had checked — pointed both at whatever
1549
+ * an attacker liked, and the delete is the half that does not come back.
1550
+ */
1551
+ const workspaceReal = realpathDeep(record.workspace);
1552
+ if (!contains(workspaceReal, realpathDeep(from))) {
1553
+ return {
1554
+ ok: false,
1555
+ because: `${from} really lives outside the workspace, so its records were not moved`,
1556
+ };
1557
+ }
1558
+ // ⚠️ THE RUN ID IS A PATH SEGMENT HERE TOO, and this one ends in a recursive
1559
+ // copy and a recursive delete. It comes off disk, so it is checked.
1560
+ const base = (0, node_path_1.join)((0, git_driver_1.projectStateDir)(cwd), 'retired', safeRunId(record.runId));
1561
+ let to = base;
1562
+ for (let n = 2; (0, node_fs_1.existsSync)(to) && n < 100; n += 1)
1563
+ to = `${base}-${n}`;
1564
+ try {
1565
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(to), { recursive: true });
1566
+ (0, node_fs_1.cpSync)(from, to, { recursive: true });
1567
+ (0, node_fs_1.rmSync)(from, { recursive: true, force: true });
1568
+ return { ok: true, at: to };
1569
+ }
1570
+ catch (e) {
1571
+ return { ok: false, because: `its own records could not be moved to safety (${e.message})` };
1572
+ }
1573
+ }
1574
+ async function reapWorkspace(git, registry, record, cwd, probe = exports.nodeProbe,
1575
+ /** A driver bound to the given checkout. See `workspaceResidue`. */
1576
+ gitInWorkspace = (dir) => new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(dir)), home = (0, node_os_1.homedir)(),
1577
+ /**
1578
+ * `run.carry_into_checkouts` - paths looply provisioned into the checkout
1579
+ * itself. Passed through to `workspaceResidue`, which explains why.
1580
+ */
1581
+ carried = []) {
1582
+ const verdict = reapVerdict(record, cwd, probe, home);
1583
+ if (!verdict.ok)
1584
+ return { reaped: false, because: verdict.because };
1585
+ /**
1586
+ * 🔴 A QUARANTINE IS CHECKED BEFORE ANY OF THE WALKS, AND IT NEVER EXPIRES.
1587
+ *
1588
+ * Everything below re-derives its answer from the filesystem, which means a
1589
+ * checkout that was refused yesterday for a reason that has since become
1590
+ * invisible — a live orphaned agent, a git config that has since been fixed —
1591
+ * is silently eligible again today. The record is the only thing that
1592
+ * remembers looply already decided, and only a person clears it.
1593
+ */
1594
+ const held = (0, workspace_quarantine_1.quarantineOf)(cwd, record.workspace);
1595
+ if (held !== null) {
1596
+ return {
1597
+ reaped: false,
1598
+ because: `it is quarantined — ${held.because}. Look at ${record.workspace}, then ` +
1599
+ `\`looply runs --release ${(0, node_path_1.basename)(record.workspace)}\` when you have`,
1600
+ };
1601
+ }
1602
+ let evidenceAt = null;
1603
+ if ((0, node_fs_1.existsSync)(record.workspace)) {
1604
+ /**
1605
+ * 🔴 `git worktree remove` WITHOUT `--force` DELETES IGNORED FILES, and the
1606
+ * first version of this function said the opposite in as many words.
1607
+ *
1608
+ * Measured: a worktree holding only `node_modules` and `.env` — both
1609
+ * ignored, so `git status` reads clean — was removed silently and both were
1610
+ * destroyed. Git refuses over MODIFIED or UNTRACKED files; ignored ones it
1611
+ * simply takes. So "git's refusal is the check" was false, and the reaper
1612
+ * would have deleted a crashed execution's `.env`, its dependencies, and its
1613
+ * `.looply/state` ledger — the evidence of why it crashed — while reporting
1614
+ * success.
1615
+ *
1616
+ * The residue check below is the real one. `--ignored` is the whole point of
1617
+ * it: without that flag it asks git exactly the question git already answers
1618
+ * with a refusal, and learns nothing new.
1619
+ */
1620
+ const residue = await workspaceResidue(gitInWorkspace(record.workspace), record.workspace, carried);
1621
+ if (residue.length > 0) {
1622
+ const blind = residue.some((r) => r.startsWith('could not be inspected'));
1623
+ const because = keptBecause(residue, record.workspace, blind);
1624
+ /**
1625
+ * 🔴 QUARANTINED ONLY WHEN THE ANSWER CANNOT BE RE-DERIVED, AND FILES ARE
1626
+ * NOT THAT CASE. Caught by a Phase 1 pin the first time this was written.
1627
+ *
1628
+ * The obvious move is to quarantine on any refusal. It is wrong, and the
1629
+ * failure is concrete: a workspace holding a `.env` is refused, the record
1630
+ * is written, the person deletes their `.env` — and `--reap` refuses for
1631
+ * ever, because a quarantine never resets. A person who tidied up their
1632
+ * own checkout can no longer retire it without a second command they have
1633
+ * no reason to know about. The reaper gets worse, not safer.
1634
+ *
1635
+ * Residue is a fact about the filesystem RIGHT NOW. Re-deriving it is
1636
+ * cheap, it is what Phase 1 does, and it self-corrects the moment the
1637
+ * person acts. Remembering it buys nothing and costs that.
1638
+ *
1639
+ * ⚠️ THE UNINSPECTABLE CASE IS THE OPPOSITE, and it is the one the plan's
1640
+ * `repoConfigTrouble` condition names. `workspaceResidue` answers
1641
+ * `could not be inspected (…)` when the checkout's git config trips
1642
+ * `repoConfigTroubleAt` — looply could not form a view at all. That is not
1643
+ * a fact about files that will change when somebody deletes one; it is a
1644
+ * checkout looply cannot reason about, and it is exactly the judgement
1645
+ * that has to survive the process that made it.
1646
+ */
1647
+ if (residue.some((r) => r.startsWith('could not be inspected'))) {
1648
+ (0, workspace_quarantine_1.quarantine)(cwd, record.workspace, because, 'reap');
1649
+ }
1650
+ return { reaped: false, because };
1651
+ }
1652
+ // The evidence leaves before the checkout does.
1653
+ const kept = retainEvidence(record, cwd);
1654
+ if (!kept.ok)
1655
+ return { reaped: false, because: kept.because };
1656
+ evidenceAt = kept.at;
1657
+ try {
1658
+ await git.removeWorktree(record.workspace);
1659
+ }
1660
+ catch (e) {
1661
+ return { reaped: false, because: `git would not remove it: ${e.message}` };
1662
+ }
1663
+ }
1664
+ // Only after the tree is gone: the administrative record, then the claim.
1665
+ await git.pruneWorktrees().catch(() => {
1666
+ /* pruning is tidying; a failure here does not make the reap wrong */
1667
+ });
1668
+ /**
1669
+ * 🔴 AND THE SLOT, WHICH THIS DID NOT GIVE BACK. `--reap` reported a run
1670
+ * "retired", removed its checkout and cleared its record — and left the
1671
+ * concurrency slot it held. So retiring a crashed run lowered the project's
1672
+ * usable concurrency by one, permanently, and the only way back was
1673
+ * `--release slot-<n>` on a slot whose record no longer existed to name it.
1674
+ *
1675
+ * `releaseSlot` is keyed on identity, so this can only ever free the slot this
1676
+ * run actually holds.
1677
+ */
1678
+ for (const s of registry.slots()) {
1679
+ if (s.record?.runId === record.runId)
1680
+ registry.releaseSlot(s.slot, record.runId);
1681
+ }
1682
+ registry.release(record.runId);
1683
+ /**
1684
+ * 🔴 AND WHAT THE EXECUTION LEFT OUTSIDE ITS CHECKOUT — including a copy of
1685
+ * the provider credential.
1686
+ *
1687
+ * Three machine-level directories are keyed on the WORKSPACE path, which is
1688
+ * right: two executions must not share provider state or a container. What
1689
+ * was missing is the other end. This removed the checkout, the record and the
1690
+ * slot and left all three behind for ever, and one of them —
1691
+ * `~/.looply/provider-sessions/<role>-<hex>` — is the transport's HOME, which
1692
+ * is where the provider writes its credential.
1693
+ *
1694
+ * ⚠️ LAST, AND NEVER THE VERDICT. The reap has already succeeded by this
1695
+ * line; a directory that will not delete is a smaller problem than a
1696
+ * workspace that cannot be retired, and turning tidying into a refusal is how
1697
+ * a reaper stops being usable. See `workspace-residue-sweep.ts`.
1698
+ */
1699
+ /**
1700
+ * The liveness the sweep needs, from the registry it already holds.
1701
+ *
1702
+ * By this line the record is released and the slot freed, so `active()` can
1703
+ * only still name this workspace if ANOTHER record does — which is exactly the
1704
+ * case where its container identities must keep their shared-path grants. A
1705
+ * probe that cannot answer classifies `unknown`, and the sweep treats that as
1706
+ * in use rather than as absent.
1707
+ */
1708
+ (0, workspace_residue_sweep_1.sweepWorkspaceResidue)(record.workspace, undefined, {
1709
+ usersOf: (ws) => registry.active().filter((r) => r.workspace === ws).map((r) => r.runId),
1710
+ });
1711
+ return evidenceAt === null ? { reaped: true } : { reaped: true, evidenceAt };
1712
+ }
1713
+ /**
1714
+ * Workspaces on disk that no live execution and no registry record account for.
1715
+ *
1716
+ * 🔴 THE LIFECYCLE HAD A THIRD HOLE, and I found this one by using the command I
1717
+ * had just written. `looply run --new-workspace` cuts the checkout BEFORE the
1718
+ * run registers, so anything that ends the run in between — a config error, a
1719
+ * dirty tree, `--dry-run`, a crash — leaves a real worktree that `looply runs`
1720
+ * cannot see and `--reap` cannot reach, because both work from registry records
1721
+ * and there is none. They accumulate silently, one per failed start.
1722
+ *
1723
+ * ⚠️ ABSENCE OF A RECORD IS NOT ENOUGH TO CALL ONE ABANDONED. A run that has
1724
+ * created its workspace and not yet registered is in exactly that state for a
1725
+ * few hundred milliseconds. So the LEASE is the second witness, as it is for a
1726
+ * slot: a workspace whose own execution state holds a lease naming a process
1727
+ * that is not proven dead is left alone.
1728
+ *
1729
+ * ⚠️ AND IT LOOKS ONLY UNDER THE WORKSPACES ROOT — never at a `--in` checkout,
1730
+ * which is the person's and which nothing here may ever remove.
1731
+ */
1732
+ function abandonedWorkspaces(cwd, registry, probe = exports.nodeProbe, home = (0, node_os_1.homedir)()) {
1733
+ const root = workspacesRoot(cwd, home);
1734
+ let names;
1735
+ try {
1736
+ names = (0, node_fs_1.readdirSync)(root);
1737
+ }
1738
+ catch {
1739
+ return { abandoned: [], busy: [] };
1740
+ }
1741
+ const busy = [];
1742
+ const claimed = new Set(registry
1743
+ .list()
1744
+ .flatMap((e) => (e.kind === 'unreadable' ? [] : [(0, node_path_1.resolve)(e.record.workspace).toLowerCase()])));
1745
+ const out = [];
1746
+ for (const name of names) {
1747
+ const path = (0, node_path_1.join)(root, name);
1748
+ try {
1749
+ if (!(0, node_fs_1.lstatSync)(path).isDirectory())
1750
+ continue;
1751
+ }
1752
+ catch {
1753
+ continue;
1754
+ }
1755
+ if (claimed.has((0, node_path_1.resolve)(path).toLowerCase()))
1756
+ continue;
1757
+ if (checkoutBusy((0, node_path_1.join)(path, git_driver_1.STATE_DIR, 'state'), probe)) {
1758
+ /**
1759
+ * 🔴 THIS USED TO `continue` SILENTLY, and the checkout then existed
1760
+ * nowhere: no row, no count, no reason, and `looply runs` printing
1761
+ * "nothing running, and nothing left behind" — which was false, with a
1762
+ * real checkout on disk that nothing would ever mention again.
1763
+ *
1764
+ * It is BUSY, not abandoned, so it is not offered for reaping; but a
1765
+ * person looking at a directory that is plainly there deserves to see it.
1766
+ */
1767
+ busy.push(path);
1768
+ continue;
1769
+ }
1770
+ out.push(path);
1771
+ }
1772
+ return { abandoned: out.sort(), busy: busy.sort() };
1773
+ }
1774
+ /**
1775
+ * Whether anything in a checkout is still using it.
1776
+ *
1777
+ * TWO FILES, AND THE SECOND ONE EXISTS BECAUSE OF A RACE THE FIRST CANNOT SEE:
1778
+ *
1779
+ * run.lease.json a run that has started. This is the real answer.
1780
+ * creating.json a run that has CUT the checkout and not yet taken its
1781
+ * lease. `--new-workspace` creates the worktree, then loads
1782
+ * config, then runs git checks, then acquires the lease —
1783
+ * seconds, on a cold repository. For that whole window the
1784
+ * checkout had no lease, so `abandonedWorkspaces` called it
1785
+ * abandoned and a concurrent `--reap` would delete a
1786
+ * starting run's checkout out from under it.
1787
+ *
1788
+ * ⚠️ NEITHER FILE NEEDS CLEANING UP. Both name a pid; once that process is gone
1789
+ * both read as "not busy", so a crash between the two leaves nothing stuck.
1790
+ *
1791
+ * ⚠️ AND IT FAILS TOWARDS BUSY. Absent is an answer; unreadable is not.
1792
+ */
1793
+ /**
1794
+ * WHICH witness is holding a checkout, in words.
1795
+ *
1796
+ * 🔴 BOTH REFUSALS HARD-CODED "still holding a run lease", and `checkoutBusy`
1797
+ * has two witnesses. So a person told that went looking for a `run.lease.json`
1798
+ * that is not there, while the thing actually holding the checkout was the
1799
+ * creation marker — a starting run, which is a completely different situation
1800
+ * and needs a completely different response.
1801
+ */
1802
+ function busyBecause(stateDir, probe) {
1803
+ if (holderAlive((0, run_lease_1.leasePathIn)(stateDir), probe))
1804
+ return 'a run lease';
1805
+ if (holderAlive((0, node_path_1.join)(stateDir, CREATING), probe, { corruptIsBusy: false })) {
1806
+ /**
1807
+ * ⚠️ "STARTING OR FINISHING", because the marker covers BOTH ends. It is
1808
+ * written when the checkout is cut and cleared as the last thing a run does,
1809
+ * so a marker with no lease beside it means either a run that has not taken
1810
+ * its lease yet or one that has already given it back and is still tidying.
1811
+ * Saying only "starting up" sent people looking for a run that was in fact
1812
+ * on its way out.
1813
+ */
1814
+ return 'a run that is still starting up or finishing';
1815
+ }
1816
+ return 'something this could not identify';
1817
+ }
1818
+ function checkoutBusy(stateDir, probe = exports.nodeProbe) {
1819
+ /**
1820
+ * ⚠️ THE TWO FILES ARE JUDGED DIFFERENTLY WHEN THEY CANNOT BE PARSED.
1821
+ *
1822
+ * A LEASE that cannot be read fails towards busy: it is written by a live run
1823
+ * on an ordinary schedule, so an unreadable one is most likely a live write
1824
+ * caught mid-rename, and treating it as free starts a second run in a checkout
1825
+ * somebody is using.
1826
+ *
1827
+ * A CREATING marker is written ONCE, atomically, at the instant the checkout
1828
+ * is cut. It cannot be caught mid-write. So an unparseable one is corruption,
1829
+ * not a race — and treating it as busy would wedge that workspace for ever,
1830
+ * invisible to `looply runs` and unreachable by `--reap`, with no file anyone
1831
+ * could point at.
1832
+ */
1833
+ if (holderAlive((0, run_lease_1.leasePathIn)(stateDir), probe))
1834
+ return true;
1835
+ return holderAlive((0, node_path_1.join)(stateDir, CREATING), probe, { corruptIsBusy: false });
1836
+ }
1837
+ function holderAlive(path, probe, opts = {}) {
1838
+ /**
1839
+ * 🔴 A PARSE FAILURE AND A READ FAILURE ARE DIFFERENT THINGS, and one flag
1840
+ * covering both was wrong for the creation marker.
1841
+ *
1842
+ * CANNOT OPEN IT (EACCES, EBUSY, EPERM, an I/O error) is a failure to ASK.
1843
+ * Nothing has been learned, so the answer is always "somebody may be here".
1844
+ * Reading that as "free" is how `--reap` deletes a starting run's checkout —
1845
+ * which by construction has no lease yet, so the marker is its only defence.
1846
+ *
1847
+ * CANNOT PARSE IT is a statement about the contents, and what it means depends
1848
+ * on the file. A LEASE is rewritten on an ordinary schedule, so unparseable
1849
+ * most likely means caught mid-write: busy. The MARKER is written once,
1850
+ * temp-then-rename, so it cannot be caught mid-write at all: unparseable means
1851
+ * corrupt, and treating corruption as busy wedges that workspace for ever.
1852
+ */
1853
+ const corruptIsBusy = opts.corruptIsBusy !== false;
1854
+ let text;
1855
+ try {
1856
+ text = (0, node_fs_1.readFileSync)(path, 'utf8');
1857
+ }
1858
+ catch (e) {
1859
+ if (e.code === 'ENOENT')
1860
+ return false;
1861
+ return true; // could not ask
1862
+ }
1863
+ try {
1864
+ const pid = JSON.parse(text).pid;
1865
+ return typeof pid !== 'number' || probe(pid) !== 'dead';
1866
+ }
1867
+ catch {
1868
+ return corruptIsBusy;
1869
+ }
1870
+ }
1871
+ /** Written the moment a workspace is cut, so the gap before the lease is covered. */
1872
+ const CREATING = 'creating.json';
1873
+ /** Whose start marker a workspace carries, so a person can be warned before clearing it. */
1874
+ function creationMarkerHolder(cwd, name, probe = exports.nodeProbe) {
1875
+ if (name === '' || /[\/]/.test(name))
1876
+ return null;
1877
+ try {
1878
+ const at = (0, node_path_1.join)(workspacesRoot(cwd), name, git_driver_1.STATE_DIR, 'state', CREATING);
1879
+ const pid = JSON.parse((0, node_fs_1.readFileSync)(at, 'utf8')).pid;
1880
+ return typeof pid === 'number' ? { pid, liveness: probe(pid) } : null;
1881
+ }
1882
+ catch {
1883
+ return null;
1884
+ }
1885
+ }
1886
+ /**
1887
+ * Drop the creation marker — the LAST thing a run does, and only then.
1888
+ *
1889
+ * 🔴 THE TIMING IS THE WHOLE DESIGN, and both extremes were shipped and wrong.
1890
+ *
1891
+ * Cleared EARLY (the moment the run had its lease and record) it reopened the
1892
+ * window it exists to close: `releaseHold` gives the lease and the record back
1893
+ * at the TOP of the teardown, with work still to do below it, so a concurrent
1894
+ * `--reap` saw no lease, no record and no marker while the run was still
1895
+ * writing.
1896
+ *
1897
+ * NEVER cleared, it becomes a permanent pid-shaped claim on a finished
1898
+ * checkout: once that pid is recycled the workspace reads as busy for ever,
1899
+ * invisible to `--reap`, with nothing a person could point at.
1900
+ *
1901
+ * So: last, after everything else the run does.
1902
+ */
1903
+ function clearCreationMarker(stateDir) {
1904
+ try {
1905
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(stateDir, CREATING));
1906
+ }
1907
+ catch {
1908
+ /* absent is the goal state */
1909
+ }
1910
+ }
1911
+ /**
1912
+ * Drop this run's addressed control file, once the run is over.
1913
+ *
1914
+ * ⚠️ NOT REQUIRED FOR CORRECTNESS EITHER — it is named for a runId that will
1915
+ * never exist again, so nothing can ever read it. It is cleared because
1916
+ * otherwise `looply stop` leaves one behind per stopped run, for ever, in a
1917
+ * directory that never had them: an ordinary Simple Mode checkout would
1918
+ * accumulate a `control.<runId>.json` every time a person pressed stop.
1919
+ *
1920
+ * ⚠️ AND AT TEARDOWN, NOT AT CONSUME. Between consuming a stop and actually
1921
+ * ending, a run is still running and a second stop is still a thing a person
1922
+ * may send it.
1923
+ */
1924
+ function clearAddressedControl(stateDir, runId) {
1925
+ try {
1926
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(stateDir, `control.${runId}.json`));
1927
+ }
1928
+ catch {
1929
+ /* absent is the goal state */
1930
+ }
1931
+ }
1932
+ /**
1933
+ * Retire a workspace that no record accounts for.
1934
+ *
1935
+ * Everything the record-based reap checks, minus the record: containment on the
1936
+ * real path, residue, evidence retention, and `worktree remove` without
1937
+ * `--force`. A person's `--in` checkout can never reach this — it is not under
1938
+ * the workspaces root, which is checked first and by realpath.
1939
+ */
1940
+ async function reapAbandoned(git, cwd, path, probe = exports.nodeProbe, gitInWorkspace = (dir) => new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(dir)), home = (0, node_os_1.homedir)(),
1941
+ /**
1942
+ * `run.carry_into_checkouts` - paths looply provisioned into the checkout
1943
+ * itself. Passed through to `workspaceResidue`, which explains why.
1944
+ */
1945
+ carried = []) {
1946
+ const rootReal = realpathDeep(workspacesRoot(cwd, home));
1947
+ const real = realpathDeep(path);
1948
+ if (!contains(rootReal, real)) {
1949
+ return { reaped: false, because: `${path} is not under this project's workspaces root` };
1950
+ }
1951
+ if (checkoutBusy((0, node_path_1.join)(path, git_driver_1.STATE_DIR, 'state'), probe)) {
1952
+ return { reaped: false, because: `something is still using it (${busyBecause((0, node_path_1.join)(path, git_driver_1.STATE_DIR, 'state'), probe)})` };
1953
+ }
1954
+ /**
1955
+ * 🔴 THE QUARANTINE IS CHECKED HERE TOO, AND LEAVING IT OUT DESTROYED THE
1956
+ * CHECKOUT THE QUARANTINE EXISTED TO PROTECT. Measured end to end.
1957
+ *
1958
+ * `reapWorkspace` consulted the quarantine; this — the function `--reap`
1959
+ * calls for every checkout with NO registry record — did not. And the two are
1960
+ * one step apart, because looply's own refusal printed
1961
+ * `looply runs --release <name>`, a workspace directory is NAMED for its
1962
+ * runId, and that command's ordered chain matches the RUN RECORD branch
1963
+ * first. So the person read the refusal, typed exactly what it said, was told
1964
+ * "cleared — the workspace was left alone", and thereby made the checkout
1965
+ * record-less: abandoned. The very next `--reap` printed the `quarantined`
1966
+ * row and then `retired`, in one invocation, and the checkout was gone with
1967
+ * the "do not touch this" record still sitting on disk.
1968
+ *
1969
+ * A protection that one branch honours and its sibling does not is not a
1970
+ * protection; it is a coin toss decided by which list a directory happens to
1971
+ * be on.
1972
+ */
1973
+ const held = (0, workspace_quarantine_1.quarantineOf)(cwd, path);
1974
+ if (held !== null) {
1975
+ return {
1976
+ reaped: false,
1977
+ because: `it is quarantined — ${held.because}. Look at ${path}, then ` +
1978
+ `\`looply runs --release ${(0, node_path_1.basename)(path)}\` when you have`,
1979
+ };
1980
+ }
1981
+ if (!(0, node_fs_1.existsSync)(path))
1982
+ return { reaped: true };
1983
+ const residue = await workspaceResidue(gitInWorkspace(path), path, carried);
1984
+ if (residue.length > 0) {
1985
+ const blind = residue.some((r) => r.startsWith('could not be inspected'));
1986
+ const because = keptBecause(residue, path, blind);
1987
+ // Same rule as `reapWorkspace`: only the answer that cannot be re-derived
1988
+ // is written down. See the note there.
1989
+ if (residue.some((r) => r.startsWith('could not be inspected'))) {
1990
+ (0, workspace_quarantine_1.quarantine)(cwd, path, because, 'reap-abandoned');
1991
+ }
1992
+ return { reaped: false, because };
1993
+ }
1994
+ /**
1995
+ * ⚠️ THE SYNTHETIC ID IS SANITISED. `retainEvidence` uses it as a path segment
1996
+ * and `safeRunId` refuses anything that is not `[A-Za-z0-9_-]` — so a
1997
+ * directory under the workspaces root with a dot or a space in its name would
1998
+ * throw out of `reapAbandoned`, and out of the `--reap` loop that calls it,
1999
+ * abandoning every workspace after it in the list.
2000
+ */
2001
+ const tag = `abandoned-${(0, node_path_1.basename)(path).replace(/[^A-Za-z0-9_-]/g, '-')}`.slice(0, 120);
2002
+ const kept = retainEvidence({ runId: tag, workspace: path }, cwd);
2003
+ if (!kept.ok)
2004
+ return { reaped: false, because: kept.because };
2005
+ try {
2006
+ await git.removeWorktree(path);
2007
+ }
2008
+ catch (e) {
2009
+ return { reaped: false, because: `git would not remove it: ${e.message}` };
2010
+ }
2011
+ await git.pruneWorktrees().catch(() => {
2012
+ /* tidying */
2013
+ });
2014
+ /**
2015
+ * 🔴 AND IT SWEEPS, WHICH IT DID NOT — so the provider-session directory, which
2016
+ * holds a COPY OF THE PROVIDER CREDENTIAL, survived every record-less reap.
2017
+ *
2018
+ * Measured: six of six seeded credential files still on disk after this
2019
+ * function reported `{reaped: true}` and removed the checkout. That is the
2020
+ * exact exposure the sweep was added for, left open on the path that handles
2021
+ * precisely the crashed-before-register workspaces which motivated it.
2022
+ */
2023
+ (0, workspace_residue_sweep_1.sweepWorkspaceResidue)(path);
2024
+ return { reaped: true };
2025
+ }
2026
+ //# sourceMappingURL=workspace.js.map