@1aboveio/skills 0.10.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 (397) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +90 -0
  3. package/bin/1aboveio-skills.mjs +18 -0
  4. package/package.json +28 -0
  5. package/runtime/skills/distribution/generated/recipes.json +1189 -0
  6. package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
  7. package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  8. package/skills/backend/airflow-dag-develop/LICENSE +3 -0
  9. package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
  10. package/skills/backend/app-debug/LICENSE +3 -0
  11. package/skills/backend/app-debug/SKILL.md +109 -0
  12. package/skills/backend/app-debug/references/common-errors.md +128 -0
  13. package/skills/backend/python-backend/LICENSE +3 -0
  14. package/skills/backend/python-backend/SKILL.md +326 -0
  15. package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
  16. package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
  17. package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
  18. package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
  19. package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
  20. package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
  21. package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
  22. package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
  23. package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
  24. package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
  25. package/skills/cicd-pipeline/docker/LICENSE +3 -0
  26. package/skills/cicd-pipeline/docker/SKILL.md +126 -0
  27. package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
  28. package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
  29. package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
  30. package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
  31. package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
  32. package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
  33. package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
  34. package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
  35. package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
  36. package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
  37. package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
  38. package/skills/cicd-pipeline/mergify/LICENSE +3 -0
  39. package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
  40. package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
  41. package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
  42. package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
  43. package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
  44. package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
  45. package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
  46. package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
  47. package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
  48. package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
  49. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
  50. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
  51. package/skills/cicd-pipeline/podman/LICENSE +3 -0
  52. package/skills/cicd-pipeline/podman/SKILL.md +70 -0
  53. package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
  54. package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  55. package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
  56. package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
  57. package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
  58. package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
  59. package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
  60. package/skills/engineering/e2e-test/LICENSE +3 -0
  61. package/skills/engineering/e2e-test/SKILL.md +156 -0
  62. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
  63. package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
  64. package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
  65. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  66. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
  67. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
  68. package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
  69. package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
  70. package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
  71. package/skills/engineering/e2e-test/references/debugging.md +36 -0
  72. package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
  73. package/skills/engineering/e2e-test/references/reviewing.md +39 -0
  74. package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
  75. package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
  76. package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
  77. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  78. package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
  79. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  80. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  81. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  82. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  83. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
  84. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  85. package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
  86. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
  87. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  88. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  89. package/skills/engineering/engineering-runtime/SKILL.md +10 -0
  90. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  91. package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
  92. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  93. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  94. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  95. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  96. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  97. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  98. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  99. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
  100. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
  101. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  102. package/skills/engineering/ensure-coverage/SKILL.md +136 -0
  103. package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
  104. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
  105. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
  106. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  107. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  108. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  109. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
  110. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  111. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
  112. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
  113. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
  114. package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
  115. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
  116. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
  117. package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
  118. package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
  119. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
  120. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  121. package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
  122. package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
  123. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
  124. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
  125. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
  126. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
  127. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
  128. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
  129. package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
  130. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
  131. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  132. package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
  133. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
  134. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  135. package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
  136. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  137. package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
  138. package/skills/engineering/harness-runtime/LICENSE +3 -0
  139. package/skills/engineering/harness-runtime/SKILL.md +18 -0
  140. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  141. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  142. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  143. package/skills/engineering/harness-runtime/contracts.md +15 -0
  144. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  145. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  146. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  147. package/skills/engineering/harness-runtime/index.mjs +767 -0
  148. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  149. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  150. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
  151. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  152. package/skills/engineering/implement-and-pr/LICENSE +3 -0
  153. package/skills/engineering/implement-and-pr/SKILL.md +176 -0
  154. package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
  155. package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
  156. package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
  157. package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
  158. package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
  159. package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
  160. package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
  161. package/skills/engineering/resolve-issues/LICENSE +3 -0
  162. package/skills/engineering/resolve-issues/SKILL.md +167 -0
  163. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
  164. package/skills/engineering/resolve-issues/references/breaker.md +82 -0
  165. package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
  166. package/skills/engineering/resolve-issues/references/delivery.md +108 -0
  167. package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
  168. package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
  169. package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
  170. package/skills/engineering/resolve-issues/references/finalization.md +68 -0
  171. package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
  172. package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
  173. package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
  174. package/skills/engineering/resolve-issues/references/intake.md +86 -0
  175. package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
  176. package/skills/engineering/resolve-issues/references/interference.md +87 -0
  177. package/skills/engineering/resolve-issues/references/loop.md +134 -0
  178. package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
  179. package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
  180. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
  181. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
  182. package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
  183. package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
  184. package/skills/engineering/resolve-issues/references/run-state.md +288 -0
  185. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
  186. package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
  187. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
  188. package/skills/engineering/resolve-issues/references/why.md +653 -0
  189. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
  190. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
  191. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
  192. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
  193. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
  194. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
  195. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
  196. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  197. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
  198. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
  199. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
  200. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
  201. package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
  202. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
  203. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
  204. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
  205. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
  206. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
  207. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
  208. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
  209. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
  210. package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
  211. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  212. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
  213. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
  214. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
  215. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
  216. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
  217. package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
  218. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
  219. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
  220. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
  221. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
  222. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
  223. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
  224. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
  225. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
  226. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
  227. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
  228. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
  229. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
  230. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
  231. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
  232. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
  233. package/skills/engineering/resolve-release/LICENSE +3 -0
  234. package/skills/engineering/resolve-release/SKILL.md +112 -0
  235. package/skills/engineering/resolve-release/references/assembly.md +137 -0
  236. package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
  237. package/skills/engineering/resolve-release/references/candidate.md +167 -0
  238. package/skills/engineering/resolve-release/references/exposure.md +178 -0
  239. package/skills/engineering/resolve-release/references/handoff.md +24 -0
  240. package/skills/engineering/resolve-release/references/postmortem.md +230 -0
  241. package/skills/engineering/resolve-release/references/preflight.md +207 -0
  242. package/skills/engineering/resolve-release/references/principles.md +94 -0
  243. package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
  244. package/skills/engineering/resolve-release/references/related-skills.md +12 -0
  245. package/skills/engineering/resolve-release/references/routing.md +149 -0
  246. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
  247. package/skills/engineering/resolve-release/references/versioning.md +202 -0
  248. package/skills/engineering/resolve-release/references/why.md +53 -0
  249. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
  250. package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
  251. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
  252. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
  253. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
  254. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
  255. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
  256. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
  257. package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
  258. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
  259. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
  260. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
  261. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
  262. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
  263. package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
  264. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
  265. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
  266. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
  267. package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
  268. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
  269. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
  270. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
  271. package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
  272. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
  273. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
  274. package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
  275. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
  276. package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
  277. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
  278. package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
  279. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
  280. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
  281. package/skills/engineering/resolve-release/vendor/README.md +60 -0
  282. package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
  283. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
  284. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
  285. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
  286. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
  287. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  288. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
  289. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  290. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
  291. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
  292. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
  293. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
  294. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
  295. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
  296. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
  297. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
  298. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  299. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  300. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
  301. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
  302. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
  303. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
  304. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
  305. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
  306. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
  307. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
  308. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
  309. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
  310. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
  311. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
  312. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
  313. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
  314. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
  315. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  316. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  317. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
  318. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
  319. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  320. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
  321. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
  322. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
  323. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
  324. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
  325. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
  326. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
  327. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
  328. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
  329. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
  330. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
  331. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
  332. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
  333. package/skills/engineering/review-pr/LICENSE +3 -0
  334. package/skills/engineering/review-pr/SKILL.md +123 -0
  335. package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
  336. package/skills/engineering/review-pr/references/code-correctness.md +5 -0
  337. package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
  338. package/skills/engineering/review-pr/references/coordination.md +18 -0
  339. package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
  340. package/skills/engineering/review-pr/references/finding-themes.md +7 -0
  341. package/skills/engineering/review-pr/references/github-posting.md +98 -0
  342. package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
  343. package/skills/engineering/review-pr/references/incremental-output.md +16 -0
  344. package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
  345. package/skills/engineering/review-pr/references/output-format.md +99 -0
  346. package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
  347. package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
  348. package/skills/engineering/review-pr/references/re-review.md +18 -0
  349. package/skills/engineering/review-pr/references/review-method.md +199 -0
  350. package/skills/engineering/review-pr/references/review-mode.md +30 -0
  351. package/skills/engineering/review-pr/references/review-posture.md +53 -0
  352. package/skills/engineering/review-pr/references/round1-depth.md +62 -0
  353. package/skills/engineering/review-pr/references/scripts.md +17 -0
  354. package/skills/engineering/review-pr/references/workflow.md +16 -0
  355. package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
  356. package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
  357. package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
  358. package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
  359. package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
  360. package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
  361. package/skills/engineering/smoke/LICENSE +3 -0
  362. package/skills/engineering/smoke/SKILL.md +131 -0
  363. package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
  364. package/skills/engineering/smoke/references/manifest.md +192 -0
  365. package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
  366. package/skills/fullstack/better-auth/LICENSE +3 -0
  367. package/skills/fullstack/better-auth/SKILL.md +601 -0
  368. package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
  369. package/skills/fullstack/monorepo/LICENSE +3 -0
  370. package/skills/fullstack/monorepo/SKILL.md +465 -0
  371. package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
  372. package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
  373. package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
  374. package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
  375. package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
  376. package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
  377. package/skills/fullstack/prisma-setup/LICENSE +3 -0
  378. package/skills/fullstack/prisma-setup/SKILL.md +180 -0
  379. package/skills/fullstack/prisma-setup/nextjs.md +258 -0
  380. package/skills/fullstack/prisma-setup/turborepo.md +301 -0
  381. package/skills/fullstack/shadcn/LICENSE +3 -0
  382. package/skills/fullstack/shadcn/SKILL.md +119 -0
  383. package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
  384. package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
  385. package/skills/fullstack/shadcn/cli.md +411 -0
  386. package/skills/fullstack/shadcn/customization.md +224 -0
  387. package/skills/fullstack/shadcn/evals/evals.json +90 -0
  388. package/skills/fullstack/shadcn/mcp.md +101 -0
  389. package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
  390. package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
  391. package/skills/fullstack/shadcn/rules/composition.md +195 -0
  392. package/skills/fullstack/shadcn/rules/data-table.md +201 -0
  393. package/skills/fullstack/shadcn/rules/forms.md +255 -0
  394. package/skills/fullstack/shadcn/rules/icons.md +103 -0
  395. package/skills/fullstack/shadcn/rules/styling.md +167 -0
  396. package/skills/fullstack/zod-v4/LICENSE +3 -0
  397. package/skills/fullstack/zod-v4/SKILL.md +287 -0
@@ -0,0 +1,829 @@
1
+ // workspaces — a tree per unit, and a reap that cannot destroy work (ADR 0001 decisions
2
+ // 3 and 4).
3
+ //
4
+ // A WORKSPACE belongs to a UNIT, from its first producer spawn until the unit is delivered
5
+ // or handed back. Fix rounds reuse it, so a dependency install is paid once per unit rather
6
+ // than once per round, and a spawn that dies leaves a partial its replacement resumes in
7
+ // place.
8
+ //
9
+ // Decision 4 is what most of this file is: **preserved on death, reaped only on success.**
10
+ // Nothing is destroyed while it holds unpublished work. That makes every rule here fail
11
+ // CLOSED — unreadable, unrecognized and still-in-use all preserve — because the two errors
12
+ // are not symmetric. A workspace kept too long is a directory a human deletes; a workspace
13
+ // removed too early is work that existed nowhere else.
14
+ //
15
+ // The defect this addresses is live, not hypothetical: `git worktree list` in this repo on
16
+ // 2026-07-26 showed ten leftovers across four naming schemes, at ONE workspace per run.
17
+ // Per-unit multiplies that by the roster, so the reap lands with the parallelism.
18
+ //
19
+ // node workspaces.mjs path <slug> <unitId> [--repo-dir DIR] # the path a unit's workspace takes
20
+ // node workspaces.mjs reap <slug> [--apply] [--repo-dir DIR] # plan (default) / perform the reap
21
+ // node workspaces.mjs --help
22
+ //
23
+ // EXIT CODES (reap):
24
+ // 0 every recorded workspace is accounted for AND the orphan sweep ran; nothing was
25
+ // preserved for cause
26
+ // 1 something was preserved or refused, or (under --apply) a removal FAILED and the tree
27
+ // is still there — read the rows before touching anything by hand
28
+ // 2 usage error
29
+ // 3 no manifest for that slug (absent is normal — run-state's own convention)
30
+ // 4 the orphan sweep could NOT run (no workspace root recorded, or `git worktree list`
31
+ // could not be read). Nothing is wrong with what was checked — the point is that the
32
+ // disk was not checked, and that used to exit 0 looking like a clean sweep. It
33
+ // OUTRANKS 1: the orchestrator's checkout is refused on every real run, so a code
34
+ // ranked under 1 would never be seen
35
+
36
+ import fs from 'node:fs'
37
+ import path from 'node:path'
38
+ import { execFileSync } from 'node:child_process'
39
+
40
+ import { terminalDisposition } from './terminal-dispositions.mjs'
41
+ // One rule for where a run's manifest lives. A second copy here would be a reap reading a
42
+ // different file from the one the run writes — and reaping against it. The import is
43
+ // one-way on purpose: `run-state.mjs` must not import this module back, because a cycle
44
+ // plus the entrypoint's top-level await deadlocks the CLI (caught by the bundled-script
45
+ // --help screen, which is what that screen is for).
46
+ import { fileFor, readManifest } from './run-state.mjs'
47
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
48
+
49
+ const isObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v)
50
+ const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0
51
+ const arr = (v) => (Array.isArray(v) ? v : [])
52
+
53
+ // ---- where a unit's workspace lives ----
54
+
55
+ // Beside the repo, never inside it: a workspace inside the checkout lands in `git status`,
56
+ // in every unit's diff, and in the glob every test discovery walks. Namespaced by run AND
57
+ // unit — two units sharing a tree is the corruption this whole change exists to remove, so
58
+ // the path derivation must make it impossible rather than merely unlikely.
59
+ export function unitWorkspacePathFor(toplevel, slug, unitId) {
60
+ const base = String(toplevel || '').replace(/\/+$/, '')
61
+ const safe = (v, fallback) => {
62
+ const s = String(v ?? '').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '')
63
+ return s.length ? s : fallback
64
+ }
65
+ return `${base}-workspaces/${safe(slug, 'run')}/${safe(unitId, 'unit')}`
66
+ }
67
+
68
+ // ---- who still holds a workspace ----
69
+
70
+ // The unit owns its tree until it is DELIVERED or HANDED BACK. Everything short of that —
71
+ // queued, in-progress, merge-ready-awaiting-delivery, blocked — still holds it.
72
+ //
73
+ // `blocked` is deliberately holding. A blocked unit is parked on a human decision, and its
74
+ // partial is exactly what that human comes back to; reaping it would delete the evidence
75
+ // the decision is about. `merge-ready` with no terminal outcome is the epic-986 shape: it
76
+ // reads as finished everywhere and is still in flight.
77
+ export function unitHoldsWorkspace(unit) {
78
+ if (!isObject(unit)) return false
79
+ const disposition = terminalDisposition(unit)
80
+ if (disposition && disposition !== 'blocked') return false
81
+ return true
82
+ }
83
+
84
+ // ---- observing a workspace ----
85
+
86
+ function git(args, cwd) {
87
+ try {
88
+ return { ok: true, out: execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) }
89
+ } catch (error) {
90
+ return { ok: false, out: '', error }
91
+ }
92
+ }
93
+
94
+ // Which processes are running INSIDE a tree. `lsof -t +D <path>` lists every PID with a file
95
+ // or cwd under that directory; it is slow on huge trees but this runs once per workspace at
96
+ // reap time, not in a loop.
97
+ //
98
+ // Four answers, and the last two are NOT the same fact:
99
+ // [] nothing holds it (including lsof's exit-1-found-nothing)
100
+ // ['123', ...] these PIDs do — preserve
101
+ // 'unreadable' lsof exists and the call failed — preserve, same reasoning as an
102
+ // unreadable `git status` not being allowed to read as clean
103
+ // 'unavailable' lsof is not installed here — the check cannot run at all; see the catch
104
+ //
105
+ // Deliberately not a process-name allowlist: what matters is that SOMETHING holds the tree,
106
+ // not what it is. A node test runner, a Playwright worker and a shell all lose work the same
107
+ // way. See the preserve branch in reapDecision for the incident.
108
+ export function liveProcessesIn(workspacePath, { run = execFileSync } = {}) {
109
+ const p = String(workspacePath ?? '')
110
+ if (!p) return []
111
+ let out
112
+ try {
113
+ out = run('lsof', ['-t', '+D', p], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
114
+ } catch (error) {
115
+ // lsof exits 1 when it simply found nothing: a clean negative, not a failure.
116
+ if (error && error.status === 1 && !String(error.stdout ?? '').trim()) return []
117
+ // NOT INSTALLED is a different fact from RAN AND FAILED, and conflating them bricks the
118
+ // reap. `lsof` is absent on a stock GitHub Linux runner, so treating its absence as
119
+ // "unreadable" made every workspace preserve — the reap stopped removing anything and
120
+ // four pre-existing tests went red. Caught by CI on this very change.
121
+ //
122
+ // The liveness check is an ADDITION to a ladder that was already safe on tree contents
123
+ // alone. Where the tool does not exist, degrading to that prior behaviour is right;
124
+ // refusing to reap anything is not. Where the tool DOES exist and failed, something is
125
+ // wrong on this machine and caution is right. Hence two values, not one — and the
126
+ // caller surfaces `unavailable` in the plan so the downgrade is visible rather than a
127
+ // guard that quietly stopped guarding.
128
+ if (error && (error.code === 'ENOENT' || error.status === 127)) return 'unavailable'
129
+ return 'unreadable'
130
+ }
131
+ const pids = String(out ?? '').split('\n').map((l) => l.trim()).filter(Boolean)
132
+ const self = new Set([String(process.pid), String(process.ppid)])
133
+ return [...new Set(pids.filter((pid) => !self.has(pid)))]
134
+ }
135
+
136
+ // Everything the reap decision needs, read from the tree itself. Three separate kinds of
137
+ // unpublished work, because they fail differently:
138
+ //
139
+ // dirty modified tracked files — what `git status` is usually checked for.
140
+ // untracked files nobody has ever committed, let alone pushed. Cheap to overlook and
141
+ // total to lose.
142
+ // unpushed commits contained in no remote-tracking ref. This is the one a status-only
143
+ // check calls CLEAN: the tree is spotless and the entire unit lives in a
144
+ // branch that exists on one machine.
145
+ export function observeWorkspace(workspacePath, { runGit = git, liveProcesses = liveProcessesIn } = {}) {
146
+ const p = String(workspacePath ?? '')
147
+ const empty = { exists: false, readable: false, dirty: [], untracked: [], unpushed: [], branch: null, head: null, gitCommonDir: null, liveProcesses: [] }
148
+ if (!p || !fs.existsSync(p)) return empty
149
+
150
+ const live = liveProcesses(p)
151
+ const head = runGit(['rev-parse', 'HEAD'], p)
152
+ if (!head.ok) return { ...empty, exists: true, liveProcesses: live }
153
+ const status = runGit(['status', '--porcelain'], p)
154
+ // An unreadable status is NOT "clean" — that is the fabricated-evidence direction, and
155
+ // here it costs work rather than a review round.
156
+ if (!status.ok) return { ...empty, exists: true, head: head.out.trim(), liveProcesses: live }
157
+
158
+ const lines = status.out.split('\n').map((l) => l.replace(/\s+$/, '')).filter(Boolean)
159
+ const untracked = lines.filter((l) => l.startsWith('??'))
160
+ const dirty = lines.filter((l) => !l.startsWith('??'))
161
+
162
+ // Contained in NO remote-tracking ref. Worktrees share the object store and the remotes
163
+ // with their parent, so this reads the same refs a push would have updated.
164
+ const revs = runGit(['rev-list', '--max-count=50', 'HEAD', '--not', '--remotes'], p)
165
+ if (!revs.ok) return { exists: true, readable: false, dirty, untracked, unpushed: [], branch: null, head: head.out.trim(), liveProcesses: live }
166
+ const unpushed = revs.out.split('\n').map((l) => l.trim()).filter(Boolean)
167
+
168
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], p)
169
+ const common = runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], p)
170
+ return {
171
+ exists: true,
172
+ readable: true,
173
+ dirty,
174
+ untracked,
175
+ unpushed,
176
+ branch: branch.ok ? branch.out.trim() : null,
177
+ head: head.out.trim(),
178
+ gitCommonDir: common.ok ? common.out.trim() : null,
179
+ liveProcesses: live,
180
+ }
181
+ }
182
+
183
+ // ---- the reap decision ----
184
+
185
+ // Trailing slashes are noise. The filesystem ROOT is not — `/` is a real directory, and
186
+ // stripping it to `''` is what turned a declared scope of `/` into "this run declared no
187
+ // scope" and a protected `/` into "nothing to protect". Both are the set-but-read-as-unset
188
+ // class (#591 F1): a value somebody set, silently read as absent, one layer down from where
189
+ // it was written. `''` stays `''`, because that is what unrecorded genuinely looks like.
190
+ const norm = (p) => {
191
+ const s = String(p ?? '')
192
+ return s.replace(/\/+$/, '') || (s.startsWith('/') ? '/' : '')
193
+ }
194
+
195
+ // "Nobody recorded a path", as distinct from "somebody recorded a bad one": absent, null,
196
+ // or a string with nothing in it. Exactly `run-state.mjs`'s `unrecordedPath` plus nullish,
197
+ // so the validator and the guard carve out the same set rather than two overlapping ones.
198
+ //
199
+ // This is the predicate the whole class turns on. PRESENCE and USABILITY are two questions,
200
+ // and `nonEmpty(v)` only answers the second — reading its `false` as "unset" is what let a
201
+ // non-string, a whitespace-only string and an all-slash root each arrive as a declaration
202
+ // nobody made, and be replaced by a scope the manifest never named. A value that is present
203
+ // and unusable must reach the guard and be REFUSED there, never routed around.
204
+ const unrecorded = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim().length === 0)
205
+
206
+ // `parent` is, or contains, `child` — both already canonical absolute paths.
207
+ //
208
+ // `${parent}/` is the obvious way to write the prefix test and it is wrong at exactly one
209
+ // value: for `parent === '/'` it is `//`, which prefixes nothing. So the one directory that
210
+ // contains every path there is answered "contains none" — and that is not academic, it is
211
+ // what let a tree resolving to `/` walk past the protected-path guard and come back REAP.
212
+ const contains = (parent, child) => (
213
+ parent === child || (parent === '/' ? child.startsWith('/') : child.startsWith(`${parent}/`))
214
+ )
215
+
216
+ // The filesystem root is never a tree this loop owns, on any side of any decision: as a
217
+ // sweep scope it is every worktree on the machine, as a candidate it is the whole disk.
218
+ // Refused on its own account rather than as a consequence of some protected path happening
219
+ // to be configured, because "it depended on unrelated configuration" is the shape of the
220
+ // last three defects here.
221
+ const isFilesystemRoot = (ident) => isObject(ident) && ident.resolved === true && ident.real === '/'
222
+
223
+ // A path is a directory on disk, not a string. Compared as text, any SECOND SPELLING of one
224
+ // directory walks past every guard below — and the second spelling is not exotic: on macOS
225
+ // `/tmp` IS a symlink to `/private/tmp`, so a workspace created as `/tmp/x` and a guard
226
+ // written as `/private/tmp/x` name one tree and match nothing.
227
+ //
228
+ // There are three such spellings, and CASE is the one a symlink resolver does not close.
229
+ // `fs.realpathSync` — the JS implementation — resolves links but hands the case back
230
+ // untouched: on a case-insensitive filesystem (default APFS, and NTFS) it answers
231
+ // `/PRIVATE/TMP` for `/PRIVATE/TMP` while that IS `/private/tmp`, and the guard then ranks
232
+ // the orchestrator's own checkout REAP under a shouted name. `fs.realpathSync.native` asks
233
+ // the OS instead of walking the links in JS, so it answers with the real on-disk spelling.
234
+ // That is the default here, and it is deliberately not backed by a fallback: falling back to
235
+ // the JS resolver would restore the case-blind compare invisibly, and every OS this runs on
236
+ // has shipped `.native` since Node 9.2. If it were ever absent the call throws inside the
237
+ // loop below, which fails CLOSED — the direction that cannot lose work.
238
+ //
239
+ // What `.native` changes BEYOND case, checked rather than assumed (Node 22, darwin/linux):
240
+ // error codes are identical on every path this walk depends on — ENOENT for a missing entry
241
+ // and for a dangling symlink, ENOTDIR through a non-directory, ELOOP on a symlink cycle — so
242
+ // the ENOENT walk-up below is unaffected and a not-yet-created path still resolves through
243
+ // its nearest real ancestor. The one behavioural difference is at the empty string, where
244
+ // the JS implementation resolves to the process CWD and `.native` throws ENOENT; that input
245
+ // is already refused above, and `.native`'s answer is the safer of the two anyway.
246
+ //
247
+ // Returns { recorded, real, resolved, reason }. `resolved: false` is the answer that
248
+ // matters — it means "this path's identity is unknowable", and every caller here treats
249
+ // that as protected, because the whole point of a guard is that uncertainty must not
250
+ // authorize a deletion.
251
+ //
252
+ // A path that does not exist YET is NOT uncertainty: nothing is there to destroy, and a
253
+ // unit's workspace path is derived before the worktree is added. So resolution walks up to
254
+ // the nearest ancestor that DOES exist and re-appends the rest — `/tmp/run/u1` resolves to
255
+ // `/private/tmp/run/u1` whether or not `u1` was created. Only the ancestor that EXISTS gets
256
+ // canonicalized, because a segment that is not on disk has no on-disk spelling to read — the
257
+ // trailing segments are re-appended exactly as recorded. That is the right answer and not a
258
+ // residual case hole: a path whose parent does not exist cannot be inside anything the guard
259
+ // protects, and there is nothing under it to destroy. Genuine uncertainty is everything
260
+ // else: a relative path (its tree depends on the caller's cwd), a dangling symlink (it
261
+ // exists, and nobody can say what it names), a permission or symlink-loop refusal, an ENOENT
262
+ // with no resolvable ancestor at all.
263
+ // Exported so a test can pin WHICH resolver is the default, which is an implementation
264
+ // assertion on purpose. On a case-SENSITIVE filesystem — ext4, i.e. this repo's Linux CI —
265
+ // no BEHAVIOURAL test can tell the two resolvers apart, because there an uppercase spelling
266
+ // genuinely names a different directory and the case tests skip for cause. Without this pin,
267
+ // the one-identifier revert that reopens the whole case class goes green on the only machine
268
+ // that gates a merge.
269
+ export const RESOLVE_PATH = fs.realpathSync.native
270
+
271
+ export function pathIdentity(input, { realpath = RESOLVE_PATH, lstat = fs.lstatSync } = {}) {
272
+ const recorded = norm(input)
273
+ const unresolved = (reason) => ({ recorded, real: null, resolved: false, reason })
274
+ if (!recorded) return unresolved('an empty path names no tree')
275
+ // A RELATIVE path is the third spelling that evades the guard, after the symlink alias and
276
+ // the trailing slash — and it is the worst of the three, because `realpath` would resolve
277
+ // it against whatever `cwd` the reap happens to run from rather than the tree it was
278
+ // recorded against. `unitWorkspacePathFor` derives from `git rev-parse --show-toplevel`
279
+ // and `git worktree list --porcelain` prints absolute paths, so nothing legitimate is
280
+ // relative; what does arrive this way is a hand-edited manifest (`run-state.mjs validate`
281
+ // says "must be a non-empty absolute path" but only checks non-empty) or a non-string
282
+ // coerced by `norm` — `42` and `[object Object]` are both relative paths under a cwd.
283
+ if (!path.isAbsolute(recorded)) return unresolved(`${recorded} is not an absolute path, so which tree it names depends on the working directory the reap happens to run from`)
284
+
285
+ const trailing = []
286
+ let cur = recorded
287
+ // Bounded: a pathological input must not spin. 64 is far past any real workspace depth.
288
+ for (let depth = 0; depth < 64; depth++) {
289
+ try {
290
+ const real = realpath(cur)
291
+ return { recorded, real: norm(trailing.length ? path.join(real, ...trailing) : real) || '/', resolved: true, reason: null }
292
+ } catch (error) {
293
+ const code = error?.code
294
+ if (code !== 'ENOENT') {
295
+ return unresolved(`realpath refused ${cur} (${code ?? error?.message ?? 'unknown error'})`)
296
+ }
297
+ // ENOENT from realpath but the entry itself is there = a symlink whose target is
298
+ // gone. "Does not exist" and "points somewhere unknown" are different answers.
299
+ let dangling = false
300
+ try { lstat(cur); dangling = true } catch { dangling = false }
301
+ if (dangling) return unresolved(`${cur} is a symlink whose target does not exist, so what it names cannot be established`)
302
+ const parent = path.dirname(cur)
303
+ if (!parent || parent === cur) return unresolved(`no existing ancestor of ${recorded} could be resolved`)
304
+ trailing.unshift(path.basename(cur))
305
+ cur = parent
306
+ }
307
+ }
308
+ return unresolved(`gave up resolving ${recorded} after 64 levels`)
309
+ }
310
+
311
+ // True when removing `candidate` would take `protectedPath` with it, in either direction:
312
+ // the same tree, a parent of it, or a child of it. A child matters because a unit workspace
313
+ // nested inside the orchestrator's checkout is itself the decision-3 violation — it is not
314
+ // a separate tree at all.
315
+ //
316
+ // Takes IDENTITIES, not strings, and an unresolvable identity on either side collides:
317
+ // a tree nobody can identify cannot be shown to be a different tree from the checkout.
318
+ function collides(candidate, guarded) {
319
+ if (!isObject(candidate) || !isObject(guarded)) return true
320
+ if (!candidate.resolved || !guarded.resolved) return true
321
+ const a = candidate.real
322
+ const b = guarded.real
323
+ if (!a || !b) return true
324
+ return contains(b, a) || contains(a, b)
325
+ }
326
+
327
+ // The whole safety rule, as one decidable function over an observation.
328
+ //
329
+ // Order matters and is deliberate: identity and the protected-path guard run BEFORE any
330
+ // evidence is weighed, so no amount of cleanliness can talk the reap into removing the
331
+ // orchestrator's checkout. Everything after them preserves on any doubt.
332
+ //
333
+ // `claimants` is every unit the manifest says owns this path. More than one is a corrupt
334
+ // manifest, and it is refused rather than resolved — see the ambiguity guard below.
335
+ export function reapDecision({ path: candidate, unit = null, claimants = null, observation = {}, protectedPaths = [], orchestratorGitCommonDir = null, requireGitOwnership = false, identify = pathIdentity } = {}) {
336
+ const p = norm(candidate)
337
+ const id = isObject(unit) && nonEmpty(unit.id) ? unit.id : null
338
+ // `liveProcesses` rides on every row so the plan header can report a platform that
339
+ // cannot run the check at all — a downgrade the per-row reasons would never show.
340
+ const row = (verdict, reason) => ({ path: p, unit: id, verdict, reap: verdict === 'reap', reason, liveProcesses: (isObject(observation) ? observation : {}).liveProcesses ?? null })
341
+
342
+ // Identity first: a path whose tree on disk cannot be established cannot be shown to be
343
+ // outside the orchestrator's checkout, so the guard below would be deciding on a name
344
+ // rather than on a directory.
345
+ // `resolved` is a claim, and `real` is the thing every guard below actually compares. An
346
+ // identity that claims resolution without naming a tree is not a resolution — `collides`
347
+ // already refuses on an empty `real`, but only once there is a protected path to compare
348
+ // against, so with none configured a lying resolver would fall straight through to REAP.
349
+ // `pathIdentity` cannot produce that shape; an injected `identify` can, and the seam is
350
+ // exported.
351
+ const me = identify(p)
352
+ if (!isObject(me) || !me.resolved || !nonEmpty(me.real)) {
353
+ return row('refuse', `refuses ${p}: its identity on disk cannot be established — ${isObject(me) ? (me.reason ?? 'it resolved to no path at all') : 'no identity was returned'}. A path that will not resolve cannot be shown to be a different tree from the orchestrator's checkout, and uncertainty never authorizes a removal`)
354
+ }
355
+
356
+ // The filesystem root, before the protected paths, because it is the one candidate whose
357
+ // refusal must not depend on one being configured. `/`, `/.`, `/..` and a symlink to `/`
358
+ // are one directory arriving under four ordinary-looking absolute spellings, and it
359
+ // contains every protected path there could be.
360
+ if (isFilesystemRoot(me)) {
361
+ return row('refuse', `refuses ${p}: it resolves to the filesystem root (/), which is not a unit's workspace — it is every checkout, every worktree and everything else on the machine. No unit owns it and nothing here removes it`)
362
+ }
363
+
364
+ // Decision 3, enforced: the orchestrator's checkout is not a unit's workspace and a
365
+ // unit's workspace is not the orchestrator's checkout. It follows that the reap may
366
+ // never touch it — and this repo's own 2026-07-26 incident is what makes the rule
367
+ // explicit rather than assumed.
368
+ for (const guarded of arr(protectedPaths).filter((g) => !unrecorded(g))) {
369
+ const g = identify(norm(guarded))
370
+ if (!isObject(g) || !g.resolved) {
371
+ return row('refuse', `refuses ${p}: the protected path ${norm(guarded)} will not resolve on disk — ${isObject(g) ? g.reason : 'no identity was returned'}, so nothing can be shown to be a different tree from the orchestrator's checkout`)
372
+ }
373
+ if (collides(me, g)) {
374
+ const via = me.real !== me.recorded || g.real !== norm(guarded) ? ` — ${me.real} and ${g.real} are one tree under two spellings` : ''
375
+ return row('refuse', `refuses ${p}: it is, contains, or sits inside the orchestrator's own checkout (${norm(guarded)})${via}. A workspace belongs to a unit; the orchestrator's tree belongs to no unit, and removing it would take the run with it`)
376
+ }
377
+ }
378
+
379
+ // A manifest that gives one tree to two units is corrupt — `run-state.mjs validate`
380
+ // already calls it an error (ADR 0001 decision 3). Keying by path and letting the last
381
+ // claimant win drops the other one silently, and the dropped claimant is exactly the unit
382
+ // whose work may be sitting in the tree. Refusing is the only answer that cannot be wrong.
383
+ const owners = arr(claimants).filter((u) => isObject(u) && nonEmpty(u.id)).map((u) => u.id)
384
+ if (owners.length > 1) {
385
+ return row('refuse', `refuses ${p}: units ${owners.join(', ')} all claim this workspace, so the manifest does not say whose work is in it — a workspace belongs to ONE unit, and \`run-state.mjs validate\` refuses this state. Nothing here is removed on an ambiguous claim; fix the manifest, then re-run`)
386
+ }
387
+
388
+ const o = isObject(observation) ? observation : {}
389
+ if (!o.exists) return row('absent', `nothing at ${p} — recorded but not on disk (already removed, or the run never created it). Reported rather than counted as a reap`)
390
+
391
+ if (unit && unitHoldsWorkspace(unit)) {
392
+ return row('preserve', `${id} still holds this workspace — it has not been delivered or handed back (status ${unit.status ?? 'queued'}${isObject(unit.delivery) && unit.delivery.outcome ? `, delivery ${unit.delivery.outcome}` : ''}). A dead spawn's replacement resumes here`)
393
+ }
394
+
395
+ // A LIVE PROCESS is the one hazard every other check here is blind to. The rest of this
396
+ // ladder reads the tree's *contents* — clean, published, owned — and a tree can satisfy
397
+ // all of it while an agent is mid-run inside it. Removing it then does not lose committed
398
+ // work; it destroys work in flight, and it does so silently from the reaper's side.
399
+ //
400
+ // fmm-express epic-894: the orchestrator pre-authorized an agent to keep running repeat
401
+ // measurements, read its interim report as terminal, and ran `reap --apply`. The tree was
402
+ // clean and its unit delivered, so the ladder said REAP. Git de-registered the worktree and
403
+ // half-deleted node_modules under a live Playwright worker — one run collapsed with 36
404
+ // `worker process exited unexpectedly`, the next could not start at all, and the crash
405
+ // produced a red mark on a test that read as a real regression until the agent opened the
406
+ // JSON. That near-miss is the cost: a destroyed measurement can also FABRICATE evidence.
407
+ //
408
+ // Preserve rather than warn: this is the same "uncertainty never authorizes cleanup" rule
409
+ // the Git-ownership checks below apply. An unreadable process table preserves too.
410
+ if (arr(o.liveProcesses).length) {
411
+ const n = o.liveProcesses.length
412
+ return row('preserve', `${n} process${n === 1 ? ' is' : 'es are'} still running inside ${p} (${o.liveProcesses.slice(0, 3).join(', ')}${n > 3 ? `, +${n - 3} more` : ''}). A clean, published tree can still have an agent mid-run in it; removing it destroys work in flight rather than work on disk. Wait for the agent to report done AND go idle, then re-run`)
413
+ }
414
+ // `unavailable` (no lsof on this platform) deliberately does NOT preserve — see
415
+ // liveProcessesIn. `unreadable` (lsof present, call failed) does.
416
+ if (o.liveProcesses === 'unreadable') {
417
+ return row('preserve', `could not determine whether any process is running inside ${p}, so it cannot be shown to be idle — the process table is as load-bearing as the Git state here, and uncertainty never authorizes cleanup`)
418
+ }
419
+
420
+ if (!o.readable) {
421
+ return row('preserve', `git could not be read at ${p}, so nothing here can be shown to be published — unreadable is not empty`)
422
+ }
423
+
424
+ // A producer may isolate itself by replacing a linked-worktree pointer with an embedded
425
+ // standalone Git directory (`.git-local`, `.pi-git`, or any equivalent). The tree can be
426
+ // clean and published while no longer belonging to the orchestrator repo's worktree set;
427
+ // sending it to `git worktree remove` then fails late with an opaque validation error.
428
+ // Preserve it at planning time. Cleanup is intentionally reversible and remains human-
429
+ // visible: move the standalone clone aside, prune the stale parent registration, then
430
+ // send the moved clone to Trash after checking this same publication evidence.
431
+ if (requireGitOwnership && (!nonEmpty(orchestratorGitCommonDir) || !nonEmpty(o.gitCommonDir))) {
432
+ return row('preserve', `Git ownership could not be established for ${p} (workspace common dir ${o.gitCommonDir ?? 'unreadable'}, orchestrator common dir ${orchestratorGitCommonDir ?? 'unreadable'}), so the orchestrator cannot prove \`git worktree remove\` owns this path. Resolve both with \`git rev-parse --path-format=absolute --git-common-dir\`; uncertainty never authorizes cleanup`)
433
+ }
434
+ if (nonEmpty(orchestratorGitCommonDir) && nonEmpty(o.gitCommonDir)
435
+ && path.resolve(o.gitCommonDir) !== path.resolve(orchestratorGitCommonDir)) {
436
+ return row('preserve', `clean published workspace is now a standalone Git checkout in a different Git object store (${o.gitCommonDir}), so the orchestrator repo cannot remove it with \`git worktree remove\`. Move it aside, prune the stale parent worktree registration, then send the moved clone to Trash after rechecking publication; never force-remove or raw-delete it`)
437
+ }
438
+
439
+ const held = []
440
+ if (arr(o.dirty).length) held.push(`${o.dirty.length} uncommitted change${o.dirty.length === 1 ? '' : 's'}`)
441
+ if (arr(o.untracked).length) held.push(`${o.untracked.length} untracked file${o.untracked.length === 1 ? '' : 's'}`)
442
+ if (arr(o.unpushed).length) held.push(`${o.unpushed.length} commit${o.unpushed.length === 1 ? '' : 's'} on no remote (unpushed)`)
443
+ if (held.length) {
444
+ return row('preserve', `holds unpublished work: ${held.join(', ')}. Push or salvage it first — this is the one rule that never bends`)
445
+ }
446
+
447
+ return row('reap', `clean and fully published${o.branch ? ` (branch ${o.branch})` : ''}, and its unit is ${id ? 'delivered or handed back' : 'no longer in the roster'} — safe to remove`)
448
+ }
449
+
450
+ // ---- the plan ----
451
+
452
+ // Reads the manifest-recorded paths AND the worktrees actually on disk, because those two
453
+ // sets differ in exactly the case that matters: a session that died before recording the
454
+ // path it had already created. A manifest walk alone leaves those orphans forever; a disk
455
+ // walk alone loses the unit each path belongs to.
456
+ // `knownWorktrees` distinguishes three states on purpose. An array is a listing that was
457
+ // taken (empty = looked, found none); `null`/omitted means NO listing was taken or it could
458
+ // not be read — which is reported as such, never as an empty disk.
459
+ export function planReap(manifest = {}, { orchestratorCheckout = null, orchestratorGitCommonDir = null, requireGitOwnership = false, knownWorktrees = null, observe = observeWorkspace, identify = pathIdentity } = {}) {
460
+ const m = isObject(manifest) ? manifest : {}
461
+ const units = arr(m.units).filter((u) => isObject(u) && nonEmpty(u.id))
462
+
463
+ // One identity per distinct spelling, computed once: every guard below decides on the
464
+ // tree a path names rather than on the text of the path.
465
+ const cache = new Map()
466
+ const idOf = (p) => {
467
+ const k = norm(p)
468
+ if (!cache.has(k)) cache.set(k, identify(k))
469
+ return cache.get(k)
470
+ }
471
+ // A tree nobody can identify still needs a stable key, and it must never collide with
472
+ // another unidentifiable one — two unresolvable paths are not known to be the same tree.
473
+ const keyOf = (p) => {
474
+ const ident = idOf(p)
475
+ return isObject(ident) && ident.resolved ? ident.real : `unresolved:${norm(p)}`
476
+ }
477
+
478
+ // The run-level checkout is the orchestrator's own tree under `workspaceMode: worktree`.
479
+ // It is protected here for the same reason the primary checkout is: it belongs to no
480
+ // unit, and the handoff removes it separately, last, after every branch is pushed.
481
+ // Filtered on `unrecorded`, not on `nonEmpty`: a run-level `workspacePath` of `42` was
482
+ // dropped here outright, and one of `/` survived the filter only to be `norm`ed to `''`
483
+ // and dropped by the `nonEmpty` filters downstream. Either way the run's own checkout
484
+ // stopped being protected while the manifest still named it. A guard nobody can read now
485
+ // reaches `reapDecision` and refuses every candidate, which is the fail-closed answer.
486
+ const protectedPaths = [orchestratorCheckout, m.workspacePath].filter((p) => !unrecorded(p)).map(norm)
487
+
488
+ // Claims, keyed by identity rather than by spelling — so two spellings of one tree are
489
+ // one row with two claimants (ambiguous, refused), not two rows that each half-decide.
490
+ const claims = new Map()
491
+ const claim = (p, unit) => {
492
+ const key = keyOf(p)
493
+ if (!claims.has(key)) claims.set(key, { path: norm(p), units: [] })
494
+ if (unit) claims.get(key).units.push(unit)
495
+ return claims.get(key)
496
+ }
497
+ for (const u of units) {
498
+ // Same rule as everywhere else: a unit that recorded NOTHING has no tree to claim; a
499
+ // unit that recorded something unreadable does, and dropping its claim silently would
500
+ // leave that tree unowned — collectable as an orphan by a sweep that never heard of it.
501
+ if (unrecorded(u.workspacePath)) continue
502
+ claim(u.workspacePath, u)
503
+ }
504
+
505
+ // Orphans: a worktree on disk under this run's workspace root that no unit claims.
506
+ const scope = sweepScope(m, { identify: idOf, protectedPaths })
507
+ const listed = Array.isArray(knownWorktrees)
508
+ const onDisk = arr(knownWorktrees).filter((p) => !unrecorded(p)).map(norm)
509
+ let collected = 0
510
+ let unexamined = 0
511
+ let unidentified = 0
512
+ for (const wt of onDisk) {
513
+ if (claims.has(keyOf(wt))) continue
514
+ const ident = idOf(wt)
515
+ // A tree whose identity will not resolve can be placed neither INSIDE the scope nor
516
+ // outside it. Skipping it answered "outside" and dropped the row, so with no protected
517
+ // path configured the plan reported a sweep that ran and said nothing whatever about a
518
+ // tree it could not look at — #586 NOTE-1 one row down. Worse, the silence was
519
+ // conditional: with a protected path, `collides` fails closed on an unresolved identity
520
+ // and the same input DID produce a row. It gets one either way now, and `reapDecision`
521
+ // refuses it on the same rule every other unidentifiable path meets.
522
+ if (!isObject(ident) || !ident.resolved) {
523
+ claim(wt, null)
524
+ unidentified++
525
+ continue
526
+ }
527
+ const isProtected = protectedPaths.some((g) => collides(ident, idOf(g)))
528
+ const underRoot = Boolean(scope.ok && contains(scope.id.real, ident.real))
529
+ if (!isProtected && !underRoot) {
530
+ // Skipped because it is out of scope (a scope IS established and this sits outside
531
+ // it), or because there is no scope to compare it to. Only the second is a failure
532
+ // to look.
533
+ if (!scope.ok) unexamined++
534
+ continue
535
+ }
536
+ claim(wt, null)
537
+ if (!isProtected) collected++
538
+ }
539
+
540
+ // "Nothing to collect" and "I could not look" are different answers, and reporting them
541
+ // identically is what keeps orphans invisible (#586 NOTE-1). Same shape as
542
+ // `record-gate-result`: a sweep that did not run owes a REASON.
543
+ const collection = !listed
544
+ ? {
545
+ ran: false,
546
+ scanned: 0,
547
+ collected: 0,
548
+ unexamined: 0,
549
+ unidentified: 0,
550
+ reason: 'no `git worktree list` result was available, so nothing outside the manifest was examined — the trees a dead session left behind are exactly the ones the manifest does not name',
551
+ }
552
+ : !scope.ok
553
+ ? {
554
+ ran: false,
555
+ scanned: onDisk.length,
556
+ collected: 0,
557
+ unexamined,
558
+ unidentified,
559
+ reason: `this run's workspace root could not be established — ${scope.reason}. ${unexamined} worktree${unexamined === 1 ? '' : 's'} on disk ${unexamined === 1 ? 'was' : 'were'} never examined for orphans. This is "could not look", not "all clear": declare the scope with \`run-state.mjs set <slug> workspaceRoot <abs-path>\` (or record units[].workspacePath as workspaces are handed out), and read \`git worktree list\` by hand meanwhile`,
560
+ }
561
+ : { ran: true, scanned: onDisk.length, collected, unexamined: 0, unidentified, reason: null }
562
+
563
+ const decisions = [...claims.values()].map(({ path: p, units: owners }) => reapDecision({
564
+ path: p,
565
+ unit: owners[0] ?? null,
566
+ claimants: owners,
567
+ observation: observe(p),
568
+ protectedPaths,
569
+ orchestratorGitCommonDir,
570
+ requireGitOwnership,
571
+ identify: idOf,
572
+ }))
573
+
574
+ return {
575
+ root: scope.root,
576
+ protectedPaths,
577
+ collection,
578
+ decisions,
579
+ reapable: decisions.filter((d) => d.verdict === 'reap'),
580
+ preserved: decisions.filter((d) => d.verdict === 'preserve'),
581
+ refused: decisions.filter((d) => d.verdict === 'refuse'),
582
+ absent: decisions.filter((d) => d.verdict === 'absent'),
583
+ }
584
+ }
585
+
586
+ // The orphan sweep's SCOPE — the directory every one of this run's unit workspaces sits
587
+ // under — decided rather than assumed. Returns `{ root, id, ok, reason }`; `ok: false` is a
588
+ // "could not look", never a clean sweep, and it always names a reason.
589
+ //
590
+ // It used to be `dirname` of the FIRST recorded workspace path, which is arbitrary in one
591
+ // direction and dangerous in the other. Arbitrary: with several recorded paths, the first
592
+ // entry decides the scope for all of them and nothing checks that the rest agree.
593
+ // Dangerous: `dirname` of a unit mis-recorded as the checkout itself is the repo's PARENT —
594
+ // a directory holding every OTHER checkout on the machine, whose worktrees then rank as
595
+ // this run's orphans. Only clean, fully-pushed trees could ever have been removed, so the
596
+ // evidence rules bounded the blast radius; that is not the same as the scope being right.
597
+ //
598
+ // So, in order:
599
+ //
600
+ // declared `workspaceRoot` on the manifest is the run SAYING where its trees live.
601
+ // It wins outright — the recorded paths do not get a vote.
602
+ // inferred the fallback, kept because a run whose paths were chosen by an older
603
+ // scheme still deserves its orphans collected. It now requires AGREEMENT:
604
+ // every recorded path must sit under one parent TREE (two spellings of one
605
+ // directory agree; two directories do not). Disagreement yields no scope
606
+ // rather than the first entry's, which reads downstream as "could not look".
607
+ // bounded whichever way it arrived, a root that is, contains, or sits inside a
608
+ // protected tree is refused. Declared does not mean unchecked: a run may no
609
+ // more declare `/` than infer the repo's parent.
610
+ export function sweepScope(manifest, { identify = pathIdentity, protectedPaths = [] } = {}) {
611
+ const m = isObject(manifest) ? manifest : {}
612
+ const no = (root, reason) => ({ root, id: null, ok: false, reason })
613
+
614
+ // `unrecorded`, not `nonEmpty`, and `=== null`, not falsy — both halves matter, and both
615
+ // were the same mistake. DECLARED is a question about presence; whether the declaration is
616
+ // usable is a separate question, answered below by `identify` and the bounds. Deciding
617
+ // presence off "did this normalize to something truthy" is what discarded a declared `42`,
618
+ // a declared `' '` and a declared `/` alike and swept an INFERRED scope instead — the
619
+ // manifest's own words overruled by a guess, in the one direction that removes things.
620
+ // A declared root that will not resolve, or that IS the filesystem root, now falls out of
621
+ // the checks below as a refusal, which is where it belongs.
622
+ let root = unrecorded(m.workspaceRoot) ? null : norm(m.workspaceRoot)
623
+ if (root === null) {
624
+ const recorded = arr(m.units).map((u) => (isObject(u) ? u.workspacePath : null)).filter((p) => !unrecorded(p))
625
+ if (!recorded.length) {
626
+ return no(null, 'no unit on this manifest records a workspacePath and the run declares no workspaceRoot')
627
+ }
628
+ const parents = recorded.map((p) => norm(path.dirname(norm(p))))
629
+ // Keyed by identity, like every other compare in this file: `/tmp/run` and
630
+ // `/private/tmp/run` are one parent, and two unresolvable parents are never known to be.
631
+ const keys = new Set(parents.map((p) => {
632
+ const ident = identify(p)
633
+ return isObject(ident) && ident.resolved ? ident.real : `unresolved:${p}`
634
+ }))
635
+ if (keys.size !== 1) {
636
+ return no(null, `this run's ${recorded.length} recorded workspaces sit under ${keys.size} different parents (${[...new Set(parents)].join(', ')}), so none of them is the run's root — picking the first is picking one at random`)
637
+ }
638
+ root = parents[0]
639
+ }
640
+
641
+ const id = identify(root)
642
+ if (!isObject(id) || !id.resolved) {
643
+ return no(root, `${root} will not resolve on disk (${isObject(id) ? (id.reason ?? 'it resolved to no path at all') : 'no identity was returned'})`)
644
+ }
645
+ // Bounded on its own account first, because `/` is the one scope whose refusal must not
646
+ // depend on a protected path being configured — and because `/`, `//`, `/.`, `/..` and a
647
+ // symlink to `/` are five spellings of it, only the first two of which look alarming.
648
+ if (isFilesystemRoot(id)) {
649
+ return no(root, `${root} resolves to the filesystem root (/), which holds every checkout and every worktree on the machine — a scope of / proposes all of them as this run's orphans. A run may no more declare / than infer the repo's parent`)
650
+ }
651
+ for (const guarded of arr(protectedPaths).filter((g) => !unrecorded(g))) {
652
+ if (collides(id, identify(norm(guarded)))) {
653
+ return no(root, `${root} is, contains, or sits inside the orchestrator's checkout (${norm(guarded)}) — a scope holding the checkout holds every other checkout beside it too, and their worktrees are not this run's orphans`)
654
+ }
655
+ }
656
+ return { root, id, ok: true, reason: null }
657
+ }
658
+
659
+ // What root a manifest names, with no judgement about whether it is usable. `sweepScope` is
660
+ // what the reap decides on; this stays for callers that only want the path.
661
+ export function runWorkspaceRoot(manifest, opts = {}) {
662
+ return sweepScope(manifest, opts).root
663
+ }
664
+
665
+ // ---- CLI ----
666
+
667
+ // `null`, not `[]`, when git could not answer: an empty array is the claim that the disk
668
+ // was read and held nothing, which is the conflation this whole path exists to remove.
669
+ export function listWorktrees(repoDir) {
670
+ const r = git(['worktree', 'list', '--porcelain'], repoDir)
671
+ if (!r.ok) return null
672
+ return r.out.split('\n').filter((l) => l.startsWith('worktree ')).map((l) => l.slice('worktree '.length).trim())
673
+ }
674
+
675
+ export function renderPlan(plan) {
676
+ const c = plan.collection ?? { ran: false, reason: 'orphan collection reported nothing about itself' }
677
+ const out = [
678
+ 'REAP PLAN',
679
+ // No reason of its own: `root` is null for several different causes — nothing recorded,
680
+ // recorded paths that disagree about their parent, a declaration that cannot be read —
681
+ // and this line used to assert the first one for all of them. A wrong reason on a
682
+ // correct refusal sends a human looking for a field that is not missing (#591 F3). The
683
+ // one true reason is already one line down, on the collection row.
684
+ ` workspace root: ${plan.root ?? '— not established; see orphan collection below'}`,
685
+ c.ran
686
+ ? ` orphan collection: ran over ${c.scanned} worktree${c.scanned === 1 ? '' : 's'} on disk; ${c.collected} orphan${c.collected === 1 ? '' : 's'} collected${c.unidentified ? `; ${c.unidentified} could not be identified and ${c.unidentified === 1 ? 'is' : 'are'} refused below` : ''}`
687
+ : ` orphan collection: NOT RUN — ${c.reason}`,
688
+ ]
689
+ // A guard that silently stopped guarding is worse than no guard: the plan would read
690
+ // exactly as it did before the liveness check existed. Say so once, at the top, where the
691
+ // reader is already deciding whether to trust the REAP rows.
692
+ if (plan.decisions.some((d) => d.liveProcesses === 'unavailable')) {
693
+ out.push(' live-process check: NOT AVAILABLE — `lsof` is not installed here, so a tree'
694
+ + ' holding a running agent cannot be told from an idle one. Rows below are decided on'
695
+ + ' tree contents alone. Confirm your agents are done before --apply.')
696
+ }
697
+ out.push('')
698
+ for (const d of plan.decisions) {
699
+ const mark = { reap: 'REAP ', preserve: 'PRESERVE', refuse: 'REFUSE ', absent: 'absent ' }[d.verdict]
700
+ out.push(` ${mark} ${d.path}${d.unit ? ` (${d.unit})` : ' (orphan — no unit claims it)'}`)
701
+ out.push(` ${d.reason}`)
702
+ }
703
+ if (!plan.decisions.length) out.push(' nothing recorded and nothing on disk under this run — no workspaces to reap')
704
+ return out.join('\n')
705
+ }
706
+
707
+ // `listWorktrees` is a seam, and a narrow one on purpose. `--apply`'s exit fold —
708
+ // `notRemoved ||` below — is unreachable through the real CLI: `git worktree list` always
709
+ // includes the orchestrator's own checkout, which is always REFUSED, so `plan.refused`
710
+ // carries exit 1 on every real invocation and a revert of the fold is invisible. Injecting
711
+ // the listing is the smallest thing that lets a test reach the fold on the fold alone; the
712
+ // worktree, git's refusal and `applyReap` all stay real (see `workspaces.test.mjs`).
713
+ export function runCli(argv, { readManifestFor, cwd = process.cwd(), listWorktrees: listOnDisk = listWorktrees } = {}) {
714
+ const positional = []
715
+ const opts = {}
716
+ for (let i = 0; i < argv.length; i++) {
717
+ const a = argv[i]
718
+ if (a === '--apply') opts.apply = true
719
+ else if (a === '--json') opts.json = true
720
+ else if (a === '--repo-dir') opts.repoDir = argv[++i]
721
+ else if (a === '--file') opts.file = argv[++i]
722
+ else if (a === '--help' || a === '-h') opts.help = true
723
+ else if (a.startsWith('--')) { console.error(`workspaces: unrecognized flag ${a}`); return 2 }
724
+ else positional.push(a)
725
+ }
726
+ if (opts.help || !positional.length) {
727
+ console.log(`workspaces path <slug> <unitId> [--repo-dir DIR]
728
+ workspaces reap <slug> [--apply] [--json] [--repo-dir DIR] [--file PATH]
729
+
730
+ A workspace belongs to a unit, from its first producer spawn until the unit is
731
+ delivered or handed back. It is PRESERVED ON DEATH and reaped only on success —
732
+ branch pushed, nothing unpublished. \`reap\` plans by default and removes only with
733
+ --apply; it refuses the orchestrator's own checkout outright.
734
+
735
+ exit 0 = all clear · 1 = something preserved/refused, or --apply could not remove it
736
+ 2 = usage · 3 = no manifest
737
+ 4 = the orphan sweep could not run (nothing recorded to scope it, or no
738
+ \`git worktree list\`) — "could not look", never reported as "all clear"`)
739
+ return opts.help ? 0 : 2
740
+ }
741
+
742
+ const repoDir = opts.repoDir || cwd
743
+ const [cmd, slug, unitId] = positional
744
+
745
+ if (cmd === 'path') {
746
+ const top = git(['rev-parse', '--show-toplevel'], repoDir)
747
+ if (!top.ok) { console.error(`workspaces path: ${repoDir} is not a git repository`); return 2 }
748
+ if (!nonEmpty(slug) || !nonEmpty(unitId)) { console.error('workspaces path: <slug> and <unitId> are both required'); return 2 }
749
+ console.log(unitWorkspacePathFor(top.out.trim(), slug, unitId))
750
+ return 0
751
+ }
752
+
753
+ if (cmd !== 'reap') { console.error(`workspaces: unknown command ${cmd}`); return 2 }
754
+
755
+ const manifest = readManifestFor ? readManifestFor(slug, opts) : null
756
+ if (manifest == null) {
757
+ console.error(`workspaces reap: no manifest for ${slug} (absent is normal — the run dir is temp). Nothing recorded means nothing to reap BY PATH; check \`git worktree list\` by hand.`)
758
+ return 3
759
+ }
760
+ const top = git(['rev-parse', '--show-toplevel'], repoDir)
761
+ const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repoDir)
762
+ const plan = planReap(manifest, {
763
+ orchestratorCheckout: top.ok ? top.out.trim() : repoDir,
764
+ orchestratorGitCommonDir: common.ok ? common.out.trim() : null,
765
+ requireGitOwnership: true,
766
+ knownWorktrees: listOnDisk(repoDir),
767
+ })
768
+ if (!opts.json) console.log(renderPlan(plan))
769
+
770
+ let notRemoved = 0
771
+ let applied = null
772
+ if (opts.apply) {
773
+ applied = applyReapResult(plan, repoDir, { log: opts.json ? () => {} : console.log })
774
+ notRemoved = applied.removalFailures.length
775
+ } else if (plan.reapable.length && !opts.json) {
776
+ console.log(`\n ${plan.reapable.length} removable — re-run with --apply to remove them.`)
777
+ }
778
+ if (opts.json) console.log(JSON.stringify(applied ?? { collection: plan.collection, decisions: plan.decisions, removed: [], remaining: [...plan.preserved, ...plan.refused].map((row) => row.path), removalFailures: [] }, null, 2))
779
+ // 4 outranks 1 — deliberately, and this is the whole reason it is a code and not just a
780
+ // line of output. The orchestrator's own checkout is in `git worktree list` on every real
781
+ // invocation and is always REFUSED, so 1 fires almost always; a "could not look" ranked
782
+ // under it would never once be visible in the exit status, and the fix would live only in
783
+ // prose nobody reads. Both codes mean "read the rows"; 4 additionally means the disk was
784
+ // never swept, which is the one thing the rows themselves cannot tell you.
785
+ if (!plan.collection.ran) return 4
786
+ if (notRemoved || plan.preserved.length || plan.refused.length) return 1
787
+ return 0
788
+ }
789
+
790
+ // `--apply` is the only step in this loop that touches the disk, and a removal git REFUSED
791
+ // is not a removal. It used to be reported on stdout and nowhere else, so a caller scripting
792
+ // `reap --apply` read exit 0 over a printed FAILED — the same defect shape as a status-only
793
+ // reap calling an unpushed tree clean: a destructive operation reporting success it did not
794
+ // have. The count folds into exit 1 rather than taking a new code, because 1 already means
795
+ // "something was left in place, read the rows before touching anything by hand", and that is
796
+ // precisely what a refused removal is.
797
+ //
798
+ // Never `--force`, and never fall back to `fs.rm`: `git worktree remove` refusing is the
799
+ // last guard between this loop and work that exists nowhere else, and the caller who reads
800
+ // the row is better placed than this script to decide what the refusal meant.
801
+ export function applyReapResult(plan, repoDir, { remove = (p, cwd) => git(['worktree', 'remove', p], cwd), log = console.log } = {}) {
802
+ const removed = []
803
+ const removalFailures = []
804
+ for (const d of arr(isObject(plan) ? plan.reapable : null)) {
805
+ const r = remove(d.path, repoDir)
806
+ if (isObject(r) && r.ok) { removed.push(d.path); log(`removed ${d.path}`); continue }
807
+ const error = (isObject(r) && r.error?.message) || 'git refused'
808
+ removalFailures.push({ path: d.path, error })
809
+ log(`FAILED to remove ${d.path} — ${error}; left in place`)
810
+ }
811
+ const remaining = [
812
+ ...arr(plan?.preserved).map((row) => row.path),
813
+ ...arr(plan?.refused).map((row) => row.path),
814
+ ...removalFailures.map((row) => row.path),
815
+ ]
816
+ return { collection: structuredClone(plan?.collection ?? null), decisions: structuredClone(arr(plan?.decisions)), removed, remaining, removalFailures }
817
+ }
818
+
819
+ export function applyReap(plan, repoDir, options = {}) {
820
+ return applyReapResult(plan, repoDir, options).removalFailures.length
821
+ }
822
+
823
+ if (isMainModule(import.meta.url)) {
824
+ process.exit(runCli(process.argv.slice(2), {
825
+ readManifestFor: (slug, opts) => {
826
+ try { return readManifest(fileFor(slug, opts)) } catch { return null }
827
+ },
828
+ }))
829
+ }