@oisincoveney/pipeline 3.24.2 → 4.0.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 (382) hide show
  1. package/README.md +14 -2
  2. package/defaults/pipeline.yaml +23 -5
  3. package/defaults/profiles.yaml +104 -21
  4. package/dist/argo-graph.js +90 -88
  5. package/dist/argo-submit.d.ts +110 -138
  6. package/dist/argo-submit.js +363 -378
  7. package/dist/argo-workflow.d.ts +156 -177
  8. package/dist/argo-workflow.js +96 -138
  9. package/dist/backlog.js +5 -4
  10. package/dist/bench/eval-report.js +21 -12
  11. package/dist/claude-settings-config.js +23 -31
  12. package/dist/claude-user-config.js +6 -16
  13. package/dist/cli/bootstrap-commands.js +3 -3
  14. package/dist/cli/doctor.d.ts +1 -1
  15. package/dist/cli/doctor.js +176 -171
  16. package/dist/cli/factory-commands.js +13 -13
  17. package/dist/cli/format.js +92 -86
  18. package/dist/cli/loop-commands.js +21 -21
  19. package/dist/cli/mcp-gateway-commands.js +12 -12
  20. package/dist/cli/plan-commands.js +85 -116
  21. package/dist/cli/program.d.ts +3 -3
  22. package/dist/cli/program.js +41 -43
  23. package/dist/cli/run-command.d.ts +1 -1
  24. package/dist/cli/run-command.js +25 -25
  25. package/dist/cli/run-commands.js +31 -29
  26. package/dist/cli/run-resolver.d.ts +1 -1
  27. package/dist/cli/run-resolver.js +30 -32
  28. package/dist/cli/run-service.d.ts +2 -3
  29. package/dist/cli/run-service.js +229 -239
  30. package/dist/cli/submit-options.js +85 -83
  31. package/dist/cluster-doctor.js +147 -175
  32. package/dist/codex-config.js +24 -30
  33. package/dist/commands/bench-command.js +9 -7
  34. package/dist/commands/coordinator-command.js +35 -0
  35. package/dist/commands/pipeline-command.js +13 -7
  36. package/dist/commands/runner-command-command.js +39 -24
  37. package/dist/commands/runner-node-command.js +31 -0
  38. package/dist/commands/ticket/complete.js +29 -33
  39. package/dist/commands/ticket/create.js +84 -82
  40. package/dist/commands/ticket/graph-check.js +11 -11
  41. package/dist/commands/ticket/next.js +36 -36
  42. package/dist/commands/ticket/sequence.js +11 -12
  43. package/dist/commands/ticket/shared.d.ts +5 -2
  44. package/dist/commands/ticket/shared.js +63 -66
  45. package/dist/commands/ticket/start.js +58 -67
  46. package/dist/commands/ticket-command.js +2 -2
  47. package/dist/config/defaults.d.ts +139 -140
  48. package/dist/config/defaults.js +81 -80
  49. package/dist/config/lint.js +52 -64
  50. package/dist/config/load.d.ts +4 -5
  51. package/dist/config/load.js +64 -74
  52. package/dist/config/schema/catalog.d.ts +2 -1
  53. package/dist/config/schema/catalog.js +3 -2
  54. package/dist/config/schema/mcp.js +46 -52
  55. package/dist/config/schemas.d.ts +382 -528
  56. package/dist/config/schemas.js +400 -398
  57. package/dist/config/validate.d.ts +1 -2
  58. package/dist/config/validate.js +236 -201
  59. package/dist/config.d.ts +2 -2
  60. package/dist/context/repo-map.js +105 -119
  61. package/dist/coordinator/composition.js +169 -0
  62. package/dist/coordinator/config.js +71 -0
  63. package/dist/coordinator/run-owner.js +64 -0
  64. package/dist/coordinator/wave-advance.js +148 -0
  65. package/dist/coordinator/wave-scheduler.js +50 -0
  66. package/dist/credentials/broker.d.ts +6 -8
  67. package/dist/credentials/broker.js +17 -16
  68. package/dist/credentials/codex-config.js +15 -19
  69. package/dist/credentials/file-targets.js +9 -15
  70. package/dist/credentials/local-codex-auth-sync.js +55 -58
  71. package/dist/credentials/opencode-config.js +28 -32
  72. package/dist/credentials/runner.js +13 -13
  73. package/dist/effect-sync-errors.js +28 -0
  74. package/dist/factory/create-experiment.js +285 -121
  75. package/dist/factory/exec.js +10 -12
  76. package/dist/factory/git-credentials.js +4 -4
  77. package/dist/factory/stamp-answers.js +10 -11
  78. package/dist/factory/template-update.js +65 -44
  79. package/dist/factory-lane.d.ts +26 -40
  80. package/dist/factory-lane.js +26 -25
  81. package/dist/gates.js +3 -3
  82. package/dist/git-remote-url.js +17 -19
  83. package/dist/hooks.d.ts +26 -26
  84. package/dist/hooks.js +20 -22
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.js +27 -36
  87. package/dist/install-commands/claude-code.js +100 -92
  88. package/dist/install-commands/host-selection.js +3 -6
  89. package/dist/install-commands/opencode.js +264 -259
  90. package/dist/install-commands/planner.js +163 -181
  91. package/dist/install-commands/result-format.js +1 -3
  92. package/dist/install-commands/shared.js +25 -42
  93. package/dist/install-commands/writer.js +22 -25
  94. package/dist/install-commands.js +8 -8
  95. package/dist/jscpd-output.js +29 -0
  96. package/dist/json-config-merge.js +14 -20
  97. package/dist/json-line-values.js +17 -6
  98. package/dist/loop/argo-poll.d.ts +0 -1
  99. package/dist/loop/argo-poll.js +15 -95
  100. package/dist/loop/backlog-records.js +1 -3
  101. package/dist/loop/controller-deps.js +150 -161
  102. package/dist/loop/controller.js +164 -186
  103. package/dist/loop/gh-checks.js +62 -61
  104. package/dist/loop/gh-runner.js +22 -25
  105. package/dist/loop/loop-command.js +66 -58
  106. package/dist/loop/loop-controller-entrypoint.js +29 -30
  107. package/dist/loop/merge.js +36 -40
  108. package/dist/loop/run-terminal-poll.js +69 -0
  109. package/dist/mcp/gateway-config.js +25 -29
  110. package/dist/mcp/gateway-doctor.js +77 -119
  111. package/dist/mcp/gateway-reconcile.js +17 -22
  112. package/dist/mcp/gateway-runtime.js +1 -3
  113. package/dist/mcp/host-config.js +37 -40
  114. package/dist/mcp/host-renderers.js +10 -18
  115. package/dist/mcp/repo-local-backends.js +51 -54
  116. package/dist/mcp/toolhive-vmcp.js +39 -39
  117. package/dist/model-resolver.js +29 -33
  118. package/dist/moka-global-config.d.ts +78 -55
  119. package/dist/moka-global-config.js +93 -92
  120. package/dist/moka-submit.d.ts +2179 -866
  121. package/dist/moka-submit.js +97 -99
  122. package/dist/opencode-project-config.js +34 -40
  123. package/dist/package-assets.js +1 -3
  124. package/dist/path-refs.js +2 -2
  125. package/dist/pipeline-init.js +25 -25
  126. package/dist/pipeline-runtime.d.ts +15 -14
  127. package/dist/pipeline-runtime.js +51 -50
  128. package/dist/planned-node.js +6 -3
  129. package/dist/planning/compile.d.ts +5 -5
  130. package/dist/planning/compile.js +140 -139
  131. package/dist/planning/dependency-refs.js +5 -5
  132. package/dist/planning/generate.d.ts +1296 -1296
  133. package/dist/planning/generate.js +307 -314
  134. package/dist/planning/graph.d.ts +0 -1
  135. package/dist/planning/graph.js +131 -149
  136. package/dist/remote/argo/model.d.ts +104 -122
  137. package/dist/remote/argo/model.js +205 -197
  138. package/dist/remote/argo/policy.js +39 -47
  139. package/dist/remote/argo/storage.js +81 -85
  140. package/dist/remote/jobs/execution-identity.js +70 -0
  141. package/dist/remote/jobs/job-outcome.js +108 -0
  142. package/dist/remote/jobs/manifest.js +96 -0
  143. package/dist/remote/jobs/model.d.ts +158 -0
  144. package/dist/remote/jobs/model.js +162 -0
  145. package/dist/remote/jobs/node-bootstrap.js +213 -0
  146. package/dist/remote/jobs/policy.js +123 -0
  147. package/dist/remote/jobs/reconcile.js +100 -0
  148. package/dist/remote/jobs/run-spec-client.js +64 -0
  149. package/dist/remote/jobs/run-spec-path.js +31 -0
  150. package/dist/remote/jobs/run-spec-server.d.ts +34 -0
  151. package/dist/remote/jobs/run-spec-server.js +116 -0
  152. package/dist/remote/jobs/submit.d.ts +23 -0
  153. package/dist/remote/jobs/submit.js +116 -0
  154. package/dist/remote/jobs/watch.js +59 -0
  155. package/dist/remote/submit/argo-submission.d.ts +3 -1
  156. package/dist/remote/submit/argo-submission.js +53 -58
  157. package/dist/remote/submit/compilation.d.ts +2 -0
  158. package/dist/remote/submit/compilation.js +46 -49
  159. package/dist/remote/submit/event-boundary.js +62 -76
  160. package/dist/remote/submit/hook-events.d.ts +6 -0
  161. package/dist/remote/submit/io.d.ts +7 -1
  162. package/dist/remote/submit/io.js +40 -43
  163. package/dist/remote/submit/jobs-submission.d.ts +59 -0
  164. package/dist/remote/submit/jobs-submission.js +97 -0
  165. package/dist/remote/submit/jobs-submit-result.d.ts +25 -0
  166. package/dist/remote/submit/jobs-submit-result.js +34 -0
  167. package/dist/remote/submit/options.d.ts +49 -0
  168. package/dist/remote/submit/options.js +27 -0
  169. package/dist/remote/submit/service.d.ts +40 -1
  170. package/dist/remote/submit/service.js +67 -49
  171. package/dist/run-control/command-context.js +6 -8
  172. package/dist/run-control/commands.js +10 -10
  173. package/dist/run-control/contracts.d.ts +52 -101
  174. package/dist/run-control/contracts.js +83 -69
  175. package/dist/run-control/detach.js +66 -72
  176. package/dist/run-control/execution-store.js +0 -0
  177. package/dist/run-control/file-errors.js +1 -3
  178. package/dist/run-control/logical-segment.js +3 -3
  179. package/dist/run-control/next-node.js +67 -78
  180. package/dist/run-control/postgres/postgres-run-control-store.js +196 -225
  181. package/dist/run-control/resume-command.js +34 -41
  182. package/dist/run-control/run-artifacts-command.js +84 -105
  183. package/dist/run-control/run-command-domain.js +7 -11
  184. package/dist/run-control/run-control-store.js +9 -9
  185. package/dist/run-control/run-query-command.js +73 -100
  186. package/dist/run-control/run-record.js +5 -3
  187. package/dist/run-control/run-state-lock.js +5 -5
  188. package/dist/run-control/runtime-event-projection.js +38 -50
  189. package/dist/run-control/runtime-reporter.js +86 -102
  190. package/dist/run-control/stop-command.js +42 -53
  191. package/dist/run-control/store-fs-effects.js +3 -1
  192. package/dist/run-control/store-manifest.js +38 -13
  193. package/dist/run-control/store-paths.js +3 -3
  194. package/dist/run-control/store.js +1 -1
  195. package/dist/run-control/submit-result.js +21 -21
  196. package/dist/run-control/supervisor.js +78 -68
  197. package/dist/run-control/workspace.js +2 -2
  198. package/dist/run-state/git-refs.js +258 -307
  199. package/dist/runner/opencode-excludes.js +4 -4
  200. package/dist/runner/subprocess-result.js +28 -37
  201. package/dist/runner/subprocess.js +41 -41
  202. package/dist/runner/timeouts.js +14 -13
  203. package/dist/runner-command/dynamic-command.js +46 -39
  204. package/dist/runner-command/environment-commands.js +91 -0
  205. package/dist/runner-command/finalize.js +174 -153
  206. package/dist/runner-command/lifecycle-context.js +115 -130
  207. package/dist/runner-command/lifecycle.js +78 -72
  208. package/dist/runner-command/pre-schedule.js +203 -223
  209. package/dist/runner-command/run.js +450 -448
  210. package/dist/runner-command/schedule-source-options.js +8 -10
  211. package/dist/runner-command/select-ready-wave.js +39 -30
  212. package/dist/runner-command/task-descriptor.js +11 -17
  213. package/dist/runner-command-contract.d.ts +147 -157
  214. package/dist/runner-command-contract.js +403 -411
  215. package/dist/runner-event-schema.d.ts +364 -505
  216. package/dist/runner-event-schema.js +187 -175
  217. package/dist/runner-event-sink.js +37 -43
  218. package/dist/runner-output.js +6 -6
  219. package/dist/runner.d.ts +57 -37
  220. package/dist/runner.js +176 -127
  221. package/dist/runtime/actor-ids.d.ts +1 -0
  222. package/dist/runtime/actor-ids.js +2 -2
  223. package/dist/runtime/agent-node/agent-node.js +99 -110
  224. package/dist/runtime/agent-node/handoff-finalization.js +23 -24
  225. package/dist/runtime/agent-node/model-selection.js +19 -22
  226. package/dist/runtime/agent-node/output-finalization.js +174 -204
  227. package/dist/runtime/agent-node/prompt-rendering.js +208 -251
  228. package/dist/runtime/agent-node/session-execution.js +43 -39
  229. package/dist/runtime/builtins/builtins.js +218 -284
  230. package/dist/runtime/changed-files/changed-files.js +29 -34
  231. package/dist/runtime/command-executor/command-executor.js +31 -23
  232. package/dist/runtime/config-error.js +1 -3
  233. package/dist/runtime/context/context.js +35 -37
  234. package/dist/runtime/contracts/contracts.d.ts +3 -0
  235. package/dist/runtime/contracts/index.d.ts +1 -1
  236. package/dist/runtime/detached-race.js +11 -14
  237. package/dist/runtime/drain-merge/drain-merge.js +139 -164
  238. package/dist/runtime/durable-store/acquisition.js +5 -3
  239. package/dist/runtime/durable-store/durable-store.d.ts +3 -1
  240. package/dist/runtime/durable-store/postgres/client.js +29 -0
  241. package/dist/runtime/durable-store/postgres/migrate-substrate.js +34 -29
  242. package/dist/runtime/durable-store/postgres/postgres-store.js +18 -20
  243. package/dist/runtime/events/events.js +273 -331
  244. package/dist/runtime/events/index.js +2 -2
  245. package/dist/runtime/execution-envelope.js +44 -0
  246. package/dist/runtime/gates/adjudication/llm-judge.js +21 -24
  247. package/dist/runtime/gates/adjudication/structured-claim.js +15 -16
  248. package/dist/runtime/gates/adjudicator/adjudicator.js +27 -27
  249. package/dist/runtime/gates/adjudicator/index.js +2 -2
  250. package/dist/runtime/gates/contract.js +5 -9
  251. package/dist/runtime/gates/gates.js +9 -8
  252. package/dist/runtime/gates/kinds/acceptance/acceptance.js +68 -71
  253. package/dist/runtime/gates/kinds/acceptance/index.js +2 -2
  254. package/dist/runtime/gates/kinds/artifact/artifact.js +4 -4
  255. package/dist/runtime/gates/kinds/artifact/index.js +2 -2
  256. package/dist/runtime/gates/kinds/builtin/builtin.js +4 -4
  257. package/dist/runtime/gates/kinds/builtin/index.js +2 -2
  258. package/dist/runtime/gates/kinds/changed-files/changed-files.js +31 -35
  259. package/dist/runtime/gates/kinds/changed-files/index.js +2 -2
  260. package/dist/runtime/gates/kinds/command/command.js +3 -3
  261. package/dist/runtime/gates/kinds/command/index.js +2 -2
  262. package/dist/runtime/gates/kinds/json-schema/index.js +2 -2
  263. package/dist/runtime/gates/kinds/json-schema/json-schema.js +7 -6
  264. package/dist/runtime/gates/kinds/verdict/index.js +2 -2
  265. package/dist/runtime/gates/kinds/verdict/verdict.js +3 -3
  266. package/dist/runtime/gates/orchestrator.js +72 -86
  267. package/dist/runtime/gates/registry.js +1 -3
  268. package/dist/runtime/handoff.d.ts +2 -1
  269. package/dist/runtime/handoff.js +43 -46
  270. package/dist/runtime/hooks/command-hook.js +57 -71
  271. package/dist/runtime/hooks/context.js +21 -25
  272. package/dist/runtime/hooks/events.js +84 -100
  273. package/dist/runtime/hooks/execution.js +6 -12
  274. package/dist/runtime/hooks/hooks.js +34 -23
  275. package/dist/runtime/hooks/invocation.js +23 -29
  276. package/dist/runtime/hooks/module-hook.js +36 -35
  277. package/dist/runtime/hooks/policy.js +32 -29
  278. package/dist/runtime/hooks/results.js +56 -66
  279. package/dist/runtime/journal-acquisition.d.ts +3 -2
  280. package/dist/runtime/journal-acquisition.js +14 -14
  281. package/dist/runtime/json-validation/json-validation.js +51 -72
  282. package/dist/runtime/launch-plan-args.js +39 -0
  283. package/dist/runtime/local-scheduler.js +46 -48
  284. package/dist/runtime/node-execution.js +376 -445
  285. package/dist/runtime/node-protocol/node-protocol.js +36 -79
  286. package/dist/runtime/node-state-store.d.ts +4 -1
  287. package/dist/runtime/node-state-store.js +13 -57
  288. package/dist/runtime/node-state-tracker.js +46 -54
  289. package/dist/runtime/open-pull-request/open-pull-request.js +201 -187
  290. package/dist/runtime/opencode-adapter.js +23 -24
  291. package/dist/runtime/opencode-agent-name.js +6 -6
  292. package/dist/runtime/opencode-runtime.js +115 -93
  293. package/dist/runtime/opencode-server.js +39 -53
  294. package/dist/runtime/opencode-session-executor.js +312 -344
  295. package/dist/runtime/parallel-node/parallel-node.js +123 -134
  296. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +90 -73
  297. package/dist/runtime/protected-paths/protected-paths.js +70 -71
  298. package/dist/runtime/remediation/remediation.js +202 -231
  299. package/dist/runtime/retry.js +8 -10
  300. package/dist/runtime/run-journal.d.ts +2 -0
  301. package/dist/runtime/run-journal.js +7 -7
  302. package/dist/runtime/runtime-results.js +54 -69
  303. package/dist/runtime/scheduled-dependencies.d.ts +2 -1
  304. package/dist/runtime/scheduled-dependencies.js +61 -75
  305. package/dist/runtime/scheduler.js +179 -202
  306. package/dist/runtime/services/agent-node-runtime-service.js +3 -3
  307. package/dist/runtime/services/backlog-service.d.ts +1 -2
  308. package/dist/runtime/services/backlog-service.js +8 -14
  309. package/dist/runtime/services/command-executor-service.js +1 -1
  310. package/dist/runtime/services/config-io-service.js +24 -21
  311. package/dist/runtime/services/drain-merge-git-service.js +1 -1
  312. package/dist/runtime/services/file-system-service.js +9 -7
  313. package/dist/runtime/services/git-porcelain-service.js +6 -6
  314. package/dist/runtime/services/kubernetes-argo-service.d.ts +10 -3
  315. package/dist/runtime/services/kubernetes-argo-service.js +90 -84
  316. package/dist/runtime/services/kubernetes-jobs-read-service.js +115 -0
  317. package/dist/runtime/services/kubernetes-jobs-service.d.ts +37 -0
  318. package/dist/runtime/services/kubernetes-jobs-service.js +45 -0
  319. package/dist/runtime/services/kubernetes-jobs-watch-service.js +61 -0
  320. package/dist/runtime/services/kubernetes-lease-service.js +21 -0
  321. package/dist/runtime/services/mcp-gateway-service.js +143 -94
  322. package/dist/runtime/services/open-pull-request-git-service.js +1 -3
  323. package/dist/runtime/services/opencode-runtime-server-service.js +10 -12
  324. package/dist/runtime/services/opencode-sdk-service.js +7 -7
  325. package/dist/runtime/services/repo-io-service.js +10 -12
  326. package/dist/runtime/services/run-journal-file-service.js +2 -2
  327. package/dist/runtime/services/runner-command-io-service.js +48 -52
  328. package/dist/runtime/services/runner-event-sink-http-service.js +32 -39
  329. package/dist/runtime/step/step-node.js +15 -15
  330. package/dist/runtime/workflow-execution.js +56 -57
  331. package/dist/runtime/workflow-lifecycle.js +82 -101
  332. package/dist/runtime/yeet-event-record.js +104 -0
  333. package/dist/runtime/yeet-executor.js +184 -0
  334. package/dist/safe-json.js +16 -16
  335. package/dist/schedule/backlog-context.js +122 -116
  336. package/dist/schedule/baseline.js +249 -255
  337. package/dist/schedule/passes/coverage.js +63 -68
  338. package/dist/schedule/passes/drain-merge.js +34 -40
  339. package/dist/schedule/passes/ids.js +15 -17
  340. package/dist/schedule/passes/models.js +40 -37
  341. package/dist/schedule/passes/open-pull-request.js +16 -20
  342. package/dist/schedule/passes/references.js +18 -22
  343. package/dist/schedule/prompts.js +135 -141
  344. package/dist/schedule/scheduling-roles.js +8 -14
  345. package/dist/schema-boundary.d.ts +4 -0
  346. package/dist/schema-boundary.js +145 -0
  347. package/dist/serialized-write-queue.js +2 -2
  348. package/dist/standard-output-schemas.js +61 -59
  349. package/dist/strings.js +12 -12
  350. package/dist/task-ref.js +5 -5
  351. package/dist/tickets/apply-ticket-plan.js +52 -61
  352. package/dist/tickets/backlog-task-store.d.ts +15 -23
  353. package/dist/tickets/backlog-task-store.js +82 -94
  354. package/dist/tickets/completion/complete-ticket.js +53 -59
  355. package/dist/tickets/ticket-graph-dto.d.ts +24 -48
  356. package/dist/tickets/ticket-graph-dto.js +40 -40
  357. package/dist/tickets/ticket-graph.d.ts +7 -2
  358. package/dist/tickets/ticket-graph.js +36 -50
  359. package/dist/tickets/ticket-plan-command-args.js +6 -10
  360. package/dist/tickets/ticket-plan-render.js +10 -16
  361. package/dist/tickets/ticket-plan.d.ts +32 -41
  362. package/dist/tickets/ticket-plan.js +97 -77
  363. package/dist/tickets/ticket-selection.d.ts +3 -1
  364. package/dist/tickets/ticket-selection.js +37 -52
  365. package/dist/tickets/ticket-task-index.js +13 -17
  366. package/dist/tickets/validation-error-format.js +3 -10
  367. package/dist/token-estimator.js +12 -7
  368. package/dist/unknown-fields.js +21 -0
  369. package/dist/workflow-submit-contract.d.ts +9 -10
  370. package/dist/workflow-submit-contract.js +10 -9
  371. package/docs/adr-moka-postgres-schema-isolation.md +48 -0
  372. package/docs/adr-opencode-first-goal-loop-runtime.md +11 -11
  373. package/docs/config-architecture.md +13 -13
  374. package/docs/mcp-host-isolation.md +1 -0
  375. package/docs/moka-orchestrator-design.md +3 -3
  376. package/docs/operator-guide.md +1 -1
  377. package/docs/pipeline-console-runner-contract.md +10 -6
  378. package/docs/runtime-actor-model.md +18 -18
  379. package/docs/runtime-test-subsystems.md +200 -0
  380. package/docs/slash-command-adapter-contract.md +3 -3
  381. package/package.json +49 -85
  382. package/dist/config/schema/reference-validation.js +0 -83
@@ -2,171 +2,160 @@ import { childReporter } from "../events/events.js";
2
2
  import "../events/index.js";
3
3
  import { configUsesOpencode, leaseOpencodeRuntime } from "../opencode-runtime.js";
4
4
  import { WorktreeService, WorktreeServiceLive } from "../services/worktree-service.js";
5
- import { Effect, Semaphore } from "effect";
5
+ import { Effect, Option, Semaphore } from "effect";
6
6
  //#region src/runtime/parallel-node/parallel-node.ts
7
- function executeParallelNode(node, context, runtime) {
8
- return Effect.runPromise(Effect.provide(parallelNodeProgram(node, context, runtime), WorktreeServiceLive));
9
- }
10
- function parallelNodeProgram(node, context, runtime) {
11
- const children = node.children ?? [];
12
- if (children.length === 0) return Effect.succeed({
13
- evidence: [`parallel node '${node.id}' has no children`],
14
- exitCode: 1,
15
- output: ""
16
- });
17
- return Effect.gen(function* () {
18
- yield* gcStaleWorktrees(context);
19
- const failFast = context.plan.execution.failFast;
20
- const linkedAbort = createLinkedAbortController(context.signal);
21
- const childContext = createParallelChildContext(context, node.id, children, failFast ? linkedAbort.controller.signal : context.signal);
22
- const gate = failFast ? makeFailFastGate(linkedAbort.controller) : void 0;
23
- for (const child of children) runtime.markNodeReady(childContext, child.id);
24
- const settled = yield* runAllChildren(children, childContext, runtime, yield* makeCategorySemaphores(childContext), gate).pipe(Effect.ensuring(Effect.sync(linkedAbort.cleanup)));
25
- return aggregateParallelResult(node.id, children, settled);
26
- });
27
- }
28
- function makeFailFastGate(controller) {
29
- return {
30
- abort: () => controller.abort(),
31
- aborted: () => controller.signal.aborted
32
- };
33
- }
34
- function aggregateParallelResult(nodeId, children, settled) {
35
- const results = settled.filter((result) => result !== void 0);
36
- const failed = results.filter((result) => result.status === "failed");
37
- return {
38
- evidence: parallelEvidence(nodeId, results, failed),
39
- exitCode: failed.length > 0 ? 1 : 0,
40
- output: parallelOutput(children, results)
41
- };
42
- }
43
- function runAllChildren(children, context, runtime, caps, gate) {
44
- return Effect.forEach(children, (child) => runChildCapped(child, context, runtime, caps, gate), { concurrency: context.maxParallelNodes ?? "unbounded" });
45
- }
46
- function runChildCapped(child, context, runtime, caps, gate) {
47
- return Effect.gen(function* () {
48
- if (gate?.aborted()) return;
49
- const result = yield* withCategoryCap(caps, child.id, context, runChildInWorktree(child, context, runtime));
50
- if (gate && result.status === "failed") gate.abort();
51
- return result;
52
- });
53
- }
54
- function withCategoryCap(caps, childId, context, effect) {
55
- const category = childCategory(childId, context.config.token_budget?.fan_out_width);
56
- const semaphore = category ? caps.get(category) : void 0;
57
- return semaphore ? semaphore.withPermits(1)(effect) : effect;
58
- }
59
- function makeCategorySemaphores(context) {
60
- const fanOut = context.config.token_budget?.fan_out_width;
61
- if (!fanOut) return Effect.succeed(/* @__PURE__ */ new Map());
62
- return Effect.gen(function* () {
7
+ const makeFailFastGate = (controller) => ({
8
+ abort: () => {
9
+ controller.abort();
10
+ },
11
+ aborted: () => controller.signal.aborted
12
+ });
13
+ const makeCategorySemaphores = (context) => {
14
+ const fanOut = context.config.token_budget.fan_out_width;
15
+ return Effect.gen(function* effectBody() {
63
16
  const caps = /* @__PURE__ */ new Map();
64
17
  for (const [category, permits] of Object.entries(fanOut.by_category)) caps.set(category, yield* Semaphore.make(permits));
65
18
  return caps;
66
19
  });
67
- }
68
- function gcStaleWorktrees(context) {
69
- return Effect.gen(function* () {
70
- if (context.config.parallel_worktrees?.enabled) yield* (yield* WorktreeService).gc(context.worktreePath);
20
+ };
21
+ const gcStaleWorktrees = (context) => Effect.gen(function* effectBody() {
22
+ if (context.config.parallel_worktrees?.enabled === true) yield* (yield* WorktreeService).gc(context.worktreePath);
23
+ });
24
+ const executeChild = (child, context, runtime) => Effect.tryPromise(async () => await runtime.executeNode(child, context)).pipe(Effect.orDie);
25
+ const opencodeSessionReporter = (context) => (nodeId, sessionId) => {
26
+ context.reporter?.({
27
+ nodeId,
28
+ sessionId,
29
+ type: "node.session"
71
30
  });
72
- }
31
+ };
32
+ const leaseChildOpencodeRuntime = (context, worktreePath) => Effect.tryPromise(async () => await leaseOpencodeRuntime({
33
+ config: context.config,
34
+ ...context.reporter === void 0 ? {} : { onSession: opencodeSessionReporter(context) },
35
+ ...context.signal === void 0 ? {} : { signal: context.signal },
36
+ worktreePath
37
+ })).pipe(Effect.orDie);
38
+ const runChildWithWorktreeLease = (child, context, runtime, worktreePath) => {
39
+ const childContext = {
40
+ ...context,
41
+ worktreePath
42
+ };
43
+ if (!configUsesOpencode(context.config)) return executeChild(child, childContext, runtime);
44
+ return Effect.acquireUseRelease(leaseChildOpencodeRuntime(context, worktreePath), (childRuntime) => executeChild(child, {
45
+ ...childContext,
46
+ executor: childRuntime.executor
47
+ }, runtime), (childRuntime) => Effect.tryPromise(async () => {
48
+ await childRuntime.release();
49
+ }).pipe(Effect.orDie));
50
+ };
51
+ const childLeaseOptions = (child, context) => ({
52
+ childNodeId: child.id,
53
+ parentNodeId: context.parentParallelNodeId ?? "parallel",
54
+ repoRoot: context.worktreePath,
55
+ ...context.runId === void 0 || context.runId.length === 0 ? {} : { runId: context.runId }
56
+ });
73
57
  /**
74
58
  * PIPE-83.4: run a parallel child in its own git worktree when enabled, so
75
59
  * concurrent candidate edits can't collide. The worktree lease is acquired and
76
60
  * released as an Effect-scoped resource (released on success, failure, or
77
61
  * interruption); release retains dirty/unpushed work for downstream selection.
78
62
  */
79
- function runChildInWorktree(child, context, runtime) {
80
- if (!context.config.parallel_worktrees?.enabled) return executeChild(child, context, runtime);
81
- return Effect.gen(function* () {
63
+ const runChildInWorktree = (child, context, runtime) => {
64
+ if (context.config.parallel_worktrees?.enabled !== true) return executeChild(child, context, runtime);
65
+ return Effect.gen(function* effectBody() {
82
66
  const worktree = yield* WorktreeService;
83
67
  return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => runChildWithWorktreeLease(child, context, runtime, lease.path), (lease) => Effect.sync(() => lease.release()));
84
68
  });
85
- }
86
- function executeChild(child, context, runtime) {
87
- return Effect.tryPromise(() => runtime.executeNode(child, context)).pipe(Effect.orDie);
88
- }
89
- function runChildWithWorktreeLease(child, context, runtime, worktreePath) {
90
- const childContext = {
91
- ...context,
92
- worktreePath
93
- };
94
- if (!configUsesOpencode(context.config)) return executeChild(child, childContext, runtime);
95
- return Effect.acquireUseRelease(leaseChildOpencodeRuntime(context, worktreePath), (childRuntime) => executeChild(child, {
96
- ...childContext,
97
- executor: childRuntime.executor
98
- }, runtime), (childRuntime) => Effect.tryPromise(() => childRuntime.release()).pipe(Effect.orDie));
99
- }
100
- function leaseChildOpencodeRuntime(context, worktreePath) {
101
- return Effect.tryPromise(() => leaseOpencodeRuntime({
102
- config: context.config,
103
- ...context.reporter ? { onSession: opencodeSessionReporter(context) } : {},
104
- ...context.signal ? { signal: context.signal } : {},
105
- worktreePath
106
- })).pipe(Effect.orDie);
107
- }
108
- function opencodeSessionReporter(context) {
109
- return (nodeId, sessionId) => {
110
- context.reporter?.({
111
- nodeId,
112
- sessionId,
113
- type: "node.session"
114
- });
115
- };
116
- }
117
- function childLeaseOptions(child, context) {
118
- return {
119
- childNodeId: child.id,
120
- parentNodeId: context.parentParallelNodeId ?? "parallel",
121
- repoRoot: context.worktreePath,
122
- ...context.runId ? { runId: context.runId } : {}
123
- };
124
- }
125
- function createParallelChildContext(context, parentNodeId, children, signal) {
126
- return {
127
- ...context,
128
- hookResults: new Map(context.hookResults),
129
- nodeStateStore: context.nodeStateStore.forkForParallelChildren(children),
130
- plan: {
131
- ...context.plan,
132
- parallelBatches: [children],
133
- topologicalOrder: children
134
- },
135
- parentParallelNodeId: parentNodeId,
136
- reporter: childReporter(context, parentNodeId),
137
- ...signal ? { signal } : {}
138
- };
139
- }
140
- function createLinkedAbortController(signal) {
69
+ };
70
+ const createParallelChildContext = (context, parentNodeId, children, signal) => ({
71
+ ...context,
72
+ hookResults: new Map(context.hookResults),
73
+ nodeStateStore: context.nodeStateStore.forkForParallelChildren(children),
74
+ parentParallelNodeId: parentNodeId,
75
+ plan: {
76
+ ...context.plan,
77
+ parallelBatches: [children],
78
+ topologicalOrder: children
79
+ },
80
+ reporter: childReporter(context, parentNodeId),
81
+ ...signal === void 0 ? {} : { signal }
82
+ });
83
+ const createLinkedAbortController = (signal) => {
141
84
  const controller = new AbortController();
142
- if (!signal) return {
143
- cleanup: () => void 0,
85
+ if (signal === void 0) return {
86
+ cleanup: () => {},
144
87
  controller
145
88
  };
146
89
  if (signal.aborted) {
147
90
  controller.abort();
148
91
  return {
149
- cleanup: () => void 0,
92
+ cleanup: () => {},
150
93
  controller
151
94
  };
152
95
  }
153
- const abort = () => controller.abort();
96
+ const abort = () => {
97
+ controller.abort();
98
+ };
154
99
  signal.addEventListener("abort", abort, { once: true });
155
100
  return {
156
- cleanup: () => signal.removeEventListener("abort", abort),
101
+ cleanup: () => {
102
+ signal.removeEventListener("abort", abort);
103
+ },
157
104
  controller
158
105
  };
159
- }
160
- function childCategory(childId, fanOut) {
161
- return fanOut ? Object.keys(fanOut.by_category).find((category) => childId.includes(category)) : void 0;
162
- }
163
- function parallelEvidence(nodeId, results, failed) {
106
+ };
107
+ const childCategory = (childId, fanOut) => fanOut === void 0 ? Option.none() : Option.fromUndefinedOr(Object.keys(fanOut.by_category).find((category) => childId.includes(category)));
108
+ const withCategoryCap = (caps, childId, context, effect) => {
109
+ const category = childCategory(childId, context.config.token_budget.fan_out_width);
110
+ const semaphore = Option.flatMap(category, (value) => Option.fromUndefinedOr(caps.get(value)));
111
+ return Option.match(semaphore, {
112
+ onNone: () => effect,
113
+ onSome: (value) => value.withPermits(1)(effect)
114
+ });
115
+ };
116
+ const runChildCapped = (child, context, runtime, caps, gate) => Effect.gen(function* effectBody() {
117
+ if (gate !== void 0 && gate.aborted()) return Option.none();
118
+ const result = yield* withCategoryCap(caps, child.id, context, runChildInWorktree(child, context, runtime));
119
+ if (gate !== void 0 && result.status === "failed") gate.abort();
120
+ return Option.some(result);
121
+ });
122
+ const runAllChildren = (children, context, runtime, caps, gate) => Effect.forEach(children, (child) => runChildCapped(child, context, runtime, caps, gate), { concurrency: context.maxParallelNodes ?? "unbounded" });
123
+ const parallelEvidence = (nodeId, results, failed) => {
164
124
  if (failed.length === 0) return [`parallel node '${nodeId}' completed ${results.length} child nodes`];
165
125
  return [`parallel node '${nodeId}' failed with ${failed.length} failed child nodes`, ...failed.flatMap((result) => result.evidence)];
166
- }
167
- function parallelOutput(children, results) {
126
+ };
127
+ const parallelOutput = (children, results) => {
168
128
  const outputsByNode = new Map(results.map((result) => [result.nodeId, result.output]));
169
129
  return JSON.stringify({ children: Object.fromEntries(children.filter((child) => outputsByNode.has(child.id)).map((child) => [child.id, outputsByNode.get(child.id)])) });
170
- }
130
+ };
131
+ const aggregateParallelResult = (nodeId, children, settled) => {
132
+ const results = settled.filter(Option.isSome).map((result) => result.value);
133
+ const failed = results.filter((result) => result.status === "failed");
134
+ return {
135
+ evidence: parallelEvidence(nodeId, results, failed),
136
+ exitCode: failed.length > 0 ? 1 : 0,
137
+ output: parallelOutput(children, results)
138
+ };
139
+ };
140
+ const parallelNodeProgram = (node, context, runtime) => {
141
+ const children = node.children ?? [];
142
+ if (children.length === 0) return Effect.succeed({
143
+ evidence: [`parallel node '${node.id}' has no children`],
144
+ exitCode: 1,
145
+ output: ""
146
+ });
147
+ return Effect.gen(function* effectBody() {
148
+ yield* gcStaleWorktrees(context);
149
+ const { failFast } = context.plan.execution;
150
+ const linkedAbort = createLinkedAbortController(context.signal);
151
+ const childContext = createParallelChildContext(context, node.id, children, failFast ? linkedAbort.controller.signal : context.signal);
152
+ const gate = failFast ? makeFailFastGate(linkedAbort.controller) : void 0;
153
+ for (const child of children) runtime.markNodeReady(childContext, child.id);
154
+ const caps = yield* makeCategorySemaphores(childContext);
155
+ const settled = yield* runAllChildren(children, childContext, runtime, caps, gate).pipe(Effect.ensuring(Effect.sync(linkedAbort.cleanup)));
156
+ return aggregateParallelResult(node.id, children, settled);
157
+ });
158
+ };
159
+ const executeParallelNode = async (node, context, runtime) => await Effect.runPromise(Effect.provide(parallelNodeProgram(node, context, runtime), WorktreeServiceLive));
171
160
  //#endregion
172
161
  export { executeParallelNode };
@@ -1,3 +1,4 @@
1
+ import { literalSchema, literalsSchema, optionalSchema, parseWithSchema, stringValue, struct } from "../../schema-boundary.js";
1
2
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { execFileSync } from "node:child_process";
@@ -14,37 +15,100 @@ const WORKTREE_ROOT = ".pipeline/worktrees";
14
15
  const REGISTRY_DIR = join(WORKTREE_ROOT, "registry");
15
16
  const OWNER = "oisin-pipeline";
16
17
  const GENERATED_WORKTREE_RESOURCES = [join(".opencode", "agents"), join(".opencode", "command")];
17
- function provisionGeneratedResources(repoRoot, worktreePath) {
18
+ const provisionGeneratedResources = (repoRoot, worktreePath) => {
18
19
  for (const relativePath of GENERATED_WORKTREE_RESOURCES) {
19
20
  const source = join(repoRoot, relativePath);
20
21
  const target = join(worktreePath, relativePath);
21
22
  if (existsSync(source) && !existsSync(target)) cpSync(source, target, { recursive: true });
22
23
  }
23
- }
24
- function childWorktreeRelPath(runId, parentNodeId, childNodeId) {
25
- return join(WORKTREE_ROOT, "trees", sanitize(runId ?? "local"), sanitize(parentNodeId), sanitize(childNodeId));
26
- }
27
- function git(cwd, args) {
28
- return execFileSync("git", args, {
29
- cwd,
30
- encoding: "utf8"
31
- }).trim();
32
- }
33
- function sanitize(id) {
34
- return id.replace(/[^A-Za-z0-9._-]/g, "-");
35
- }
36
- function writeManifest(path, manifest) {
37
- writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
38
- }
39
- function readManifest(path) {
40
- return JSON.parse(readFileSync(path, "utf8"));
41
- }
42
- function createChildWorktree(opts) {
24
+ };
25
+ literalsSchema([
26
+ "active",
27
+ "removed",
28
+ "retained-dirty",
29
+ "retained-unpushed"
30
+ ]);
31
+ const worktreeManifestSchema = struct({
32
+ baseSha: stringValue(),
33
+ branch: stringValue(),
34
+ childNodeId: stringValue(),
35
+ leaseId: stringValue(),
36
+ owner: stringValue(),
37
+ parentNodeId: stringValue(),
38
+ path: stringValue(),
39
+ runId: optionalSchema(stringValue()),
40
+ schemaVersion: literalSchema(1),
41
+ state: literalsSchema([
42
+ "active",
43
+ "creating",
44
+ "removed",
45
+ "retained-dirty",
46
+ "retained-unpushed"
47
+ ])
48
+ });
49
+ const git = (cwd, args) => execFileSync("git", args, {
50
+ cwd,
51
+ encoding: "utf-8"
52
+ }).trim();
53
+ const sanitize = (id) => id.replaceAll(/[^A-Za-z0-9._-]/gu, "-");
54
+ const childWorktreeRelPath = (parentNodeId, childNodeId, runId) => join(WORKTREE_ROOT, "trees", sanitize(runId ?? "local"), sanitize(parentNodeId), sanitize(childNodeId));
55
+ const writeManifest = (path, manifest) => {
56
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
57
+ };
58
+ const readManifest = (path) => parseWithSchema(worktreeManifestSchema, JSON.parse(readFileSync(path, "utf-8")));
59
+ const retentionState = (absPath, baseSha) => {
60
+ if (git(absPath, [
61
+ "status",
62
+ "--porcelain",
63
+ "--untracked-files=all"
64
+ ]).length > 0) return "retained-dirty";
65
+ if (git(absPath, ["rev-parse", "HEAD"]) !== baseSha) return "retained-unpushed";
66
+ return "remove";
67
+ };
68
+ /** Idempotent, crash-safe teardown. Retains (never deletes) dirty/unpushed work. */
69
+ const releaseWorktree = (repoRoot, manifestPath) => {
70
+ if (!existsSync(manifestPath)) return "removed";
71
+ const manifest = readManifest(manifestPath);
72
+ const absPath = join(repoRoot, manifest.path);
73
+ git(repoRoot, ["worktree", "prune"]);
74
+ if (!existsSync(absPath)) {
75
+ writeManifest(manifestPath, {
76
+ ...manifest,
77
+ state: "removed"
78
+ });
79
+ return "removed";
80
+ }
81
+ const guarded = retentionState(absPath, manifest.baseSha);
82
+ if (guarded !== "remove") {
83
+ writeManifest(manifestPath, {
84
+ ...manifest,
85
+ state: guarded
86
+ });
87
+ return guarded;
88
+ }
89
+ git(repoRoot, [
90
+ "worktree",
91
+ "remove",
92
+ "--force",
93
+ absPath
94
+ ]);
95
+ git(repoRoot, [
96
+ "branch",
97
+ "-D",
98
+ manifest.branch
99
+ ]);
100
+ writeManifest(manifestPath, {
101
+ ...manifest,
102
+ state: "removed"
103
+ });
104
+ return "removed";
105
+ };
106
+ const createChildWorktree = (opts) => {
43
107
  const runSeg = sanitize(opts.runId ?? "local");
44
108
  const parentSeg = sanitize(opts.parentNodeId);
45
109
  const childSeg = sanitize(opts.childNodeId);
46
110
  const baseSha = git(opts.repoRoot, ["rev-parse", "HEAD"]);
47
- const relPath = childWorktreeRelPath(opts.runId, opts.parentNodeId, opts.childNodeId);
111
+ const relPath = childWorktreeRelPath(opts.parentNodeId, opts.childNodeId, opts.runId);
48
112
  const absPath = join(opts.repoRoot, relPath);
49
113
  const branch = `pipeline/worktrees/${runSeg}/${parentSeg}/${childSeg}`;
50
114
  const leaseId = `${runSeg}__${parentSeg}__${childSeg}`;
@@ -84,61 +148,14 @@ function createChildWorktree(opts) {
84
148
  path: absPath,
85
149
  release: () => releaseWorktree(opts.repoRoot, manifestPath)
86
150
  };
87
- }
88
- /** Idempotent, crash-safe teardown. Retains (never deletes) dirty/unpushed work. */
89
- function releaseWorktree(repoRoot, manifestPath) {
90
- if (!existsSync(manifestPath)) return "removed";
91
- const manifest = readManifest(manifestPath);
92
- const absPath = join(repoRoot, manifest.path);
93
- git(repoRoot, ["worktree", "prune"]);
94
- if (!existsSync(absPath)) {
95
- writeManifest(manifestPath, {
96
- ...manifest,
97
- state: "removed"
98
- });
99
- return "removed";
100
- }
101
- const guarded = retentionState(absPath, manifest.baseSha);
102
- if (guarded) {
103
- writeManifest(manifestPath, {
104
- ...manifest,
105
- state: guarded
106
- });
107
- return guarded;
108
- }
109
- git(repoRoot, [
110
- "worktree",
111
- "remove",
112
- "--force",
113
- absPath
114
- ]);
115
- git(repoRoot, [
116
- "branch",
117
- "-D",
118
- manifest.branch
119
- ]);
120
- writeManifest(manifestPath, {
121
- ...manifest,
122
- state: "removed"
123
- });
124
- return "removed";
125
- }
126
- /** Returns a retention reason when the worktree must be kept, else undefined. */
127
- function retentionState(absPath, baseSha) {
128
- if (git(absPath, [
129
- "status",
130
- "--porcelain",
131
- "--untracked-files=all"
132
- ]).length > 0) return "retained-dirty";
133
- if (git(absPath, ["rev-parse", "HEAD"]) !== baseSha) return "retained-unpushed";
134
- }
151
+ };
135
152
  /** Startup GC: release every pipeline-owned lease using the same safety guard. */
136
- function gcParallelWorktrees(repoRoot) {
153
+ const gcParallelWorktrees = (repoRoot) => {
137
154
  const registryAbs = join(repoRoot, REGISTRY_DIR);
138
155
  if (!existsSync(registryAbs)) return [];
139
- const results = readdirSync(registryAbs).sort().filter((file) => file.endsWith(".json")).map((file) => join(registryAbs, file)).filter((manifestPath) => readManifest(manifestPath).owner === OWNER).map((manifestPath) => releaseWorktree(repoRoot, manifestPath));
156
+ const results = readdirSync(registryAbs).toSorted().filter((file) => file.endsWith(".json")).map((file) => join(registryAbs, file)).filter((manifestPath) => readManifest(manifestPath).owner === OWNER).map((manifestPath) => releaseWorktree(repoRoot, manifestPath));
140
157
  git(repoRoot, ["worktree", "prune"]);
141
158
  return results;
142
- }
159
+ };
143
160
  //#endregion
144
161
  export { createChildWorktree, gcParallelWorktrees };
@@ -1,97 +1,96 @@
1
1
  import { globSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { Option } from "effect";
3
4
  //#region src/runtime/protected-paths/protected-paths.ts
4
5
  /**
5
6
  * Build the per-path `deny` overlay shared by the transports' permission maps.
6
7
  * Keeping the pattern → action shaping here makes the protected set a single
7
8
  * owner rather than two ad-hoc maps.
8
9
  */
9
- function protectedPermissionOverlay(patterns) {
10
- return Object.fromEntries(patterns.map((pattern) => [pattern, "deny"]));
11
- }
12
- /**
13
- * Snapshot the files matching `patterns` inside `worktreePath` and return a
14
- * guard that can later revert any tampering. Configuring no patterns yields a
15
- * no-op guard (no globbing, no filesystem reads).
16
- */
17
- function createProtectedPathGuard(worktreePath, patterns) {
18
- const safePatterns = (patterns ?? []).filter((pattern) => pattern.trim().length > 0);
19
- const snapshot = snapshotProtectedFiles(worktreePath, safePatterns);
20
- return {
21
- patterns: safePatterns,
22
- verifyAndRestore: () => verifyAndRestoreSnapshot(snapshot)
23
- };
24
- }
25
- function snapshotProtectedFiles(worktreePath, patterns) {
26
- if (patterns.length === 0) return [];
27
- const root = resolve(worktreePath);
28
- const entries = /* @__PURE__ */ new Map();
29
- for (const pattern of patterns) for (const match of globSync(pattern, { cwd: root })) {
30
- const entry = snapshotEntry(root, match);
31
- if (entry) entries.set(entry.absPath, entry);
32
- }
33
- return [...entries.values()];
34
- }
35
- function snapshotEntry(root, match) {
36
- const absPath = resolve(root, match);
37
- if (!isWithinRoot(root, absPath)) return;
38
- const bytes = regularFileBytes(absPath);
39
- if (!bytes) return;
40
- return {
41
- absPath,
42
- bytes,
43
- relPath: relative(root, absPath)
44
- };
45
- }
46
- function verifyAndRestoreSnapshot(snapshot) {
47
- const violations = [];
48
- for (const entry of snapshot) {
49
- const violation = verifyEntry(entry);
50
- if (violation) violations.push(violation);
51
- }
52
- return violations;
53
- }
54
- function verifyEntry(entry) {
55
- const current = regularFileBytes(entry.absPath);
56
- if (!current) {
57
- restoreEntry(entry);
58
- return {
59
- kind: "deleted",
60
- path: entry.relPath
61
- };
62
- }
63
- if (current.equals(entry.bytes)) return;
64
- restoreEntry(entry);
65
- return {
66
- kind: "modified",
67
- path: entry.relPath
68
- };
69
- }
70
- function restoreEntry(entry) {
10
+ const protectedPermissionOverlay = (patterns) => Object.fromEntries(patterns.map((pattern) => [pattern, "deny"]));
11
+ const restoreEntry = (entry) => {
71
12
  rmSync(entry.absPath, {
72
13
  force: true,
73
14
  recursive: true
74
15
  });
75
16
  mkdirSync(dirname(entry.absPath), { recursive: true });
76
17
  writeFileSync(entry.absPath, entry.bytes);
77
- }
78
- function regularFileBytes(absPath) {
18
+ };
19
+ const regularFileBytes = (absPath) => {
79
20
  let stats;
80
21
  try {
81
22
  stats = statSync(absPath);
82
23
  } catch {
83
- return;
24
+ return Option.none();
84
25
  }
85
- if (!stats.isFile()) return;
26
+ if (!stats.isFile()) return Option.none();
86
27
  try {
87
- return readFileSync(absPath);
28
+ return Option.some(readFileSync(absPath));
88
29
  } catch {
89
- return;
30
+ return Option.none();
31
+ }
32
+ };
33
+ const verifyEntry = (entry) => {
34
+ const current = regularFileBytes(entry.absPath);
35
+ if (Option.isNone(current)) {
36
+ restoreEntry(entry);
37
+ return Option.some({
38
+ kind: "deleted",
39
+ path: entry.relPath
40
+ });
90
41
  }
91
- }
92
- function isWithinRoot(root, absPath) {
42
+ if (current.value.equals(entry.bytes)) return Option.none();
43
+ restoreEntry(entry);
44
+ return Option.some({
45
+ kind: "modified",
46
+ path: entry.relPath
47
+ });
48
+ };
49
+ const verifyAndRestoreSnapshot = (snapshot) => {
50
+ const violations = [];
51
+ for (const entry of snapshot) {
52
+ const violation = verifyEntry(entry);
53
+ if (Option.isSome(violation)) violations.push(violation.value);
54
+ }
55
+ return violations;
56
+ };
57
+ const isWithinRoot = (root, absPath) => {
93
58
  const rel = relative(root, absPath);
94
59
  return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
95
- }
60
+ };
61
+ const snapshotEntry = (root, match) => {
62
+ const absPath = resolve(root, match);
63
+ if (!isWithinRoot(root, absPath)) return Option.none();
64
+ const bytes = regularFileBytes(absPath);
65
+ if (Option.isNone(bytes)) return Option.none();
66
+ return Option.some({
67
+ absPath,
68
+ bytes: bytes.value,
69
+ relPath: relative(root, absPath)
70
+ });
71
+ };
72
+ const snapshotProtectedFiles = (worktreePath, patterns) => {
73
+ if (patterns.length === 0) return [];
74
+ const root = resolve(worktreePath);
75
+ const entries = /* @__PURE__ */ new Map();
76
+ for (const pattern of patterns) for (const match of globSync(pattern, { cwd: root })) {
77
+ const entry = snapshotEntry(root, match);
78
+ if (Option.isSome(entry)) entries.set(entry.value.absPath, entry.value);
79
+ }
80
+ return [...entries.values()];
81
+ };
82
+ /**
83
+ * Snapshot the files matching `patterns` inside `worktreePath` and return a
84
+ * guard that can later revert any tampering. Configuring no patterns yields a
85
+ * no-op guard (no globbing, no filesystem reads).
86
+ */
87
+ const createProtectedPathGuard = (worktreePath, patterns) => {
88
+ const safePatterns = (patterns ?? []).filter((pattern) => pattern.trim().length > 0);
89
+ const snapshot = snapshotProtectedFiles(worktreePath, safePatterns);
90
+ return {
91
+ patterns: safePatterns,
92
+ verifyAndRestore: () => verifyAndRestoreSnapshot(snapshot)
93
+ };
94
+ };
96
95
  //#endregion
97
96
  export { createProtectedPathGuard, protectedPermissionOverlay };