@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
@@ -1,9 +1,14 @@
1
- import { PipelineConfigError, configIssuesFromZodError, validationError } from "../../config/schemas.js";
1
+ import { parseResultWithSchema } from "../../schema-boundary.js";
2
+ import { PipelineConfigError, configIssuesFromSchemaIssues, validationError } from "../../config/schemas.js";
3
+ import { throwCauseFailure, unknownErrorMessage } from "../../effect-sync-errors.js";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option$1 from "effect/Option";
2
6
  import { parseDocument } from "yaml";
3
- import { Cause, Context, Effect, Layer, Option } from "effect";
4
7
  import { existsSync, readFileSync } from "node:fs";
8
+ import * as Context from "effect/Context";
9
+ import * as Layer from "effect/Layer";
5
10
  //#region src/runtime/services/config-io-service.ts
6
- function parseYamlSource(source, sourcePath) {
11
+ const parseYamlSource = (source, sourcePath) => {
7
12
  const document = parseDocument(source, {
8
13
  prettyErrors: false,
9
14
  uniqueKeys: true
@@ -13,30 +18,28 @@ function parseYamlSource(source, sourcePath) {
13
18
  path: sourcePath
14
19
  })));
15
20
  return document.toJS();
16
- }
21
+ };
17
22
  var ConfigIoService = class extends Context.Service()("ConfigIoService") {};
18
23
  const ConfigIoServiceLive = Layer.succeed(ConfigIoService, {
19
24
  parseYaml: (source, sourcePath) => Effect.try({
20
- try: () => parseYamlSource(source, sourcePath),
21
- catch: (error) => error
25
+ catch: (error) => error instanceof PipelineConfigError ? error : new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, [{
26
+ message: unknownErrorMessage(error),
27
+ path: sourcePath
28
+ }]),
29
+ try: () => parseYamlSource(source, sourcePath)
22
30
  }),
23
- readOptionalText: (path) => Effect.sync(() => existsSync(path) ? readFileSync(path, "utf8") : null),
24
- readText: (path) => Effect.sync(() => readFileSync(path, "utf8"))
31
+ readOptionalText: (path) => Effect.sync(() => existsSync(path) ? Option$1.some(readFileSync(path, "utf-8")) : Option$1.none()),
32
+ readText: (path) => Effect.sync(() => readFileSync(path, "utf-8"))
25
33
  });
26
- function parseConfigYamlAs(source, sourcePath, schema) {
27
- return Effect.gen(function* () {
28
- const yaml = yield* (yield* ConfigIoService).parseYaml(source, sourcePath);
29
- const parsed = schema.safeParse(yaml);
30
- if (!parsed.success) return yield* Effect.fail(validationError(configIssuesFromZodError(parsed.error)));
31
- return parsed.data;
32
- });
33
- }
34
- function runConfigIoSync(program) {
34
+ const parseConfigYamlAs = (source, sourcePath, schema) => Effect.gen(function* effectBody() {
35
+ const parsed = parseResultWithSchema(schema, yield* (yield* ConfigIoService).parseYaml(source, sourcePath), { onExcessProperty: "error" });
36
+ if (!parsed.ok) return yield* Effect.fail(validationError(configIssuesFromSchemaIssues(parsed.issues)));
37
+ return parsed.value;
38
+ });
39
+ const runConfigIoSync = (program) => {
35
40
  const exit = Effect.runSyncExit(Effect.provide(program, ConfigIoServiceLive));
36
41
  if (exit._tag === "Success") return exit.value;
37
- const failure = Cause.findErrorOption(exit.cause);
38
- if (Option.isSome(failure)) throw failure.value;
39
- throw Cause.squash(exit.cause);
40
- }
42
+ return throwCauseFailure(exit.cause);
43
+ };
41
44
  //#endregion
42
45
  export { ConfigIoService, parseConfigYamlAs, runConfigIoSync };
@@ -4,7 +4,7 @@ import simpleGit from "simple-git";
4
4
  var DrainMergeGitService = class extends Context.Service()("DrainMergeGitService") {};
5
5
  const DrainMergeGitServiceLive = Layer.succeed(DrainMergeGitService, { create: (baseDir) => Effect.sync(() => {
6
6
  const git = simpleGit({ baseDir });
7
- return { raw: (args) => Effect.tryPromise(() => git.raw(args)) };
7
+ return { raw: (args) => Effect.tryPromise(async () => await git.raw(args)) };
8
8
  }) });
9
9
  //#endregion
10
10
  export { DrainMergeGitService, DrainMergeGitServiceLive };
@@ -1,19 +1,21 @@
1
- import { Cause, Context, Effect, Layer, Option } from "effect";
2
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { Cause, Context, Effect, Layer, Option } from "effect";
3
3
  //#region src/runtime/services/file-system-service.ts
4
4
  var FileSystemService = class extends Context.Service()("FileSystemService") {};
5
5
  const FileSystemServiceLive = Layer.succeed(FileSystemService, {
6
6
  exists: (path) => Effect.sync(() => existsSync(path)),
7
7
  readText: (path) => Effect.try({
8
- try: () => readFileSync(path, "utf8"),
9
- catch: (error) => error
8
+ catch: (error) => error,
9
+ try: () => readFileSync(path, "utf-8")
10
10
  }),
11
11
  writeText: (path, contents) => Effect.try({
12
- try: () => writeFileSync(path, contents),
13
- catch: (error) => error
12
+ catch: (error) => error,
13
+ try: () => {
14
+ writeFileSync(path, contents);
15
+ }
14
16
  })
15
17
  });
16
- function runFileSystemSync(program, layer) {
18
+ const runFileSystemSync = (program, layer) => {
17
19
  const exit = Effect.runSyncExit(Effect.provide(program, layer));
18
20
  switch (exit._tag) {
19
21
  case "Success": return exit.value;
@@ -24,6 +26,6 @@ function runFileSystemSync(program, layer) {
24
26
  }
25
27
  default: throw new Error("unreachable Effect exit state");
26
28
  }
27
- }
29
+ };
28
30
  //#endregion
29
31
  export { FileSystemService, FileSystemServiceLive, runFileSystemSync };
@@ -6,17 +6,18 @@ const execGit = promisify(execFile);
6
6
  var GitPorcelainService = class extends Context.Service()("GitPorcelainService") {};
7
7
  const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
8
8
  run: (cwd, args, env) => Effect.tryPromise({
9
+ catch: (error) => error,
9
10
  try: async () => {
10
11
  const { stdout } = await execGit("git", args, {
11
12
  cwd,
12
- encoding: "utf8",
13
+ encoding: "utf-8",
13
14
  env
14
15
  });
15
16
  return stdout;
16
- },
17
- catch: (error) => error
17
+ }
18
18
  }),
19
19
  statusPorcelain: (cwd) => Effect.try({
20
+ catch: (error) => error,
20
21
  try: () => execFileSync("git", [
21
22
  "status",
22
23
  "--porcelain=v1",
@@ -24,14 +25,13 @@ const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
24
25
  "-z"
25
26
  ], {
26
27
  cwd,
27
- encoding: "utf8",
28
+ encoding: "utf-8",
28
29
  stdio: [
29
30
  "ignore",
30
31
  "pipe",
31
32
  "ignore"
32
33
  ]
33
- }),
34
- catch: (error) => error
34
+ })
35
35
  })
36
36
  });
37
37
  //#endregion
@@ -1,14 +1,21 @@
1
1
  import { WorkflowReadApi } from "../../loop/argo-poll.js";
2
2
  import { Context, Effect, Layer } from "effect";
3
- import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
4
-
3
+ import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
5
4
  //#region src/runtime/services/kubernetes-argo-service.d.ts
6
5
  interface CoreApi {
7
6
  readonly createNamespacedConfigMap: (param: Parameters<CoreV1Api["createNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["createNamespacedConfigMap"]>[1]) => Promise<unknown>;
8
7
  readonly deleteNamespacedConfigMap: (param: Parameters<CoreV1Api["deleteNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["deleteNamespacedConfigMap"]>[1]) => Promise<unknown>;
9
8
  readonly patchNamespacedConfigMap: (param: Parameters<CoreV1Api["patchNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["patchNamespacedConfigMap"]>[1]) => Promise<unknown>;
10
9
  }
11
- type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject">;
10
+ interface WorkflowApi {
11
+ readonly createNamespacedCustomObject: (param: {
12
+ readonly body: unknown;
13
+ readonly group: string;
14
+ readonly namespace: string;
15
+ readonly plural: string;
16
+ readonly version: string;
17
+ }) => Promise<unknown>;
18
+ }
12
19
  interface KubernetesArgoIoDependencies {
13
20
  coreApi?: CoreApi;
14
21
  kubeConfig?: KubeConfig;
@@ -1,73 +1,110 @@
1
+ import { stringField } from "../../unknown-fields.js";
1
2
  import { classifyArgoPhase, extractArgoRawPhase } from "../../loop/argo-poll.js";
2
- import { Context, Effect, Layer } from "effect";
3
+ import { getOrElse, isSome, none, some } from "effect/Option";
3
4
  import { execa } from "execa";
5
+ import { Context, Effect, Layer } from "effect";
4
6
  import { CoreV1Api, CustomObjectsApi, KubeConfig, PatchStrategy, setHeaderOptions } from "@kubernetes/client-node";
5
7
  //#region src/runtime/services/kubernetes-argo-service.ts
8
+ const textOption = (value) => value === void 0 || value.length === 0 ? none() : some(value);
6
9
  var KubernetesArgoService = class extends Context.Service()("KubernetesArgoService") {};
10
+ const resolveKubeConfig = (options, dependencies) => {
11
+ if (dependencies.kubeConfig !== void 0) return dependencies.kubeConfig;
12
+ const kubeConfig = new KubeConfig();
13
+ const kubeconfigPath = textOption(options.kubeconfigPath);
14
+ if (isSome(kubeconfigPath)) kubeConfig.loadFromFile(kubeconfigPath.value);
15
+ else kubeConfig.loadFromDefault();
16
+ const kubeContext = textOption(options.kubeContext);
17
+ if (isSome(kubeContext)) {
18
+ if (kubeConfig.getContextObject(kubeContext.value) === null) throw new Error(`Kube context '${kubeContext.value}' was not found in the resolved kubeconfig`);
19
+ kubeConfig.setCurrentContext(kubeContext.value);
20
+ }
21
+ return kubeConfig;
22
+ };
23
+ const buildApiClients = (kubeConfig, dependencies) => ({
24
+ coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
25
+ workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
26
+ });
27
+ const apiClients = (options, dependencies) => {
28
+ if (dependencies.coreApi !== void 0 && dependencies.workflowApi !== void 0) return {
29
+ coreApi: dependencies.coreApi,
30
+ workflowApi: dependencies.workflowApi
31
+ };
32
+ return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
33
+ };
34
+ const readApiClient = (options, dependencies) => {
35
+ if (dependencies.workflowReadApi !== void 0) return dependencies.workflowReadApi;
36
+ return resolveKubeConfig(options, dependencies).makeApiClient(CustomObjectsApi);
37
+ };
38
+ const kubectlArgs = (args, kubeContext) => {
39
+ const context = textOption(kubeContext);
40
+ return isSome(context) ? [
41
+ "--context",
42
+ context.value,
43
+ ...args
44
+ ] : [...args];
45
+ };
46
+ const kubectlErrorStderr = (error) => {
47
+ const stderr = stringField(error, "stderr");
48
+ if (isSome(stderr)) return stderr.value.trim();
49
+ return getOrElse(stringField(error, "shortMessage"), () => "kubectl failed").trim();
50
+ };
51
+ const kubectlErrorStdout = (error) => getOrElse(stringField(error, "stdout"), () => "").trim();
7
52
  const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
8
53
  createConfigMap: ({ body, dependencies, namespace, options }) => Effect.try({
9
- try: () => apiClients(options, dependencies),
10
- catch: (error) => error
54
+ catch: (error) => error,
55
+ try: () => apiClients(options, dependencies)
11
56
  }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
12
- try: () => coreApi.createNamespacedConfigMap({
57
+ catch: (error) => error,
58
+ try: async () => await coreApi.createNamespacedConfigMap({
13
59
  body,
14
60
  namespace
15
- }),
16
- catch: (error) => error
17
- }))),
18
- deleteConfigMap: ({ dependencies, name, namespace, options }) => Effect.try({
19
- try: () => apiClients(options, dependencies),
20
- catch: (error) => error
21
- }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
22
- try: () => coreApi.deleteNamespacedConfigMap({
23
- name,
24
- namespace
25
- }),
26
- catch: (error) => error
61
+ })
27
62
  }))),
28
63
  createWorkflow: ({ body, dependencies, namespace, options }) => Effect.try({
29
- try: () => apiClients(options, dependencies),
30
- catch: (error) => error
64
+ catch: (error) => error,
65
+ try: () => apiClients(options, dependencies)
31
66
  }).pipe(Effect.flatMap(({ workflowApi }) => Effect.tryPromise({
32
- try: () => workflowApi.createNamespacedCustomObject({
67
+ catch: (error) => error,
68
+ try: async () => await workflowApi.createNamespacedCustomObject({
33
69
  body,
34
70
  group: "argoproj.io",
35
71
  namespace,
36
72
  plural: "workflows",
37
73
  version: "v1alpha1"
38
- }),
39
- catch: (error) => error
74
+ })
75
+ }))),
76
+ deleteConfigMap: ({ dependencies, name, namespace, options }) => Effect.try({
77
+ catch: (error) => error,
78
+ try: () => apiClients(options, dependencies)
79
+ }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
80
+ catch: (error) => error,
81
+ try: async () => await coreApi.deleteNamespacedConfigMap({
82
+ name,
83
+ namespace
84
+ })
40
85
  }))),
41
86
  getWorkflowPhase: ({ dependencies, name, namespace, options }) => Effect.try({
42
- try: () => readApiClient(options, dependencies),
43
- catch: (error) => error
87
+ catch: (error) => error,
88
+ try: () => readApiClient(options, dependencies)
44
89
  }).pipe(Effect.flatMap((workflowReadApi) => Effect.tryPromise({
45
- try: () => workflowReadApi.getNamespacedCustomObject({
90
+ catch: (error) => error,
91
+ try: async () => await workflowReadApi.getNamespacedCustomObject({
46
92
  group: "argoproj.io",
47
93
  name,
48
94
  namespace,
49
95
  plural: "workflows",
50
96
  version: "v1alpha1"
51
- }),
52
- catch: (error) => error
97
+ })
53
98
  })), Effect.map((resource) => classifyArgoPhase(extractArgoRawPhase(resource)))),
54
- patchConfigMapOwnerReferences: ({ body, dependencies, name, namespace, options }) => Effect.try({
55
- try: () => apiClients(options, dependencies),
56
- catch: (error) => error
57
- }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
58
- try: () => coreApi.patchNamespacedConfigMap({
59
- body,
60
- name,
61
- namespace
62
- }, setHeaderOptions("Content-Type", PatchStrategy.MergePatch)),
63
- catch: (error) => error
64
- }))),
65
99
  kubectl: (args, options) => Effect.tryPromise({
66
- try: () => execa("kubectl", kubectlArgs(args, options.kubeContext), {
67
- env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
68
- stdin: "ignore"
69
- }),
70
- catch: (error) => error
100
+ catch: (error) => error,
101
+ try: async () => {
102
+ const kubeconfigPath = textOption(options.kubeconfigPath);
103
+ return await execa("kubectl", kubectlArgs(args, options.kubeContext), {
104
+ env: isSome(kubeconfigPath) ? { KUBECONFIG: kubeconfigPath.value } : void 0,
105
+ stdin: "ignore"
106
+ });
107
+ }
71
108
  }).pipe(Effect.map((result) => ({
72
109
  ok: true,
73
110
  stderr: result.stderr,
@@ -76,49 +113,18 @@ const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
76
113
  ok: false,
77
114
  stderr: kubectlErrorStderr(error),
78
115
  stdout: kubectlErrorStdout(error)
116
+ }))),
117
+ patchConfigMapOwnerReferences: ({ body, dependencies, name, namespace, options }) => Effect.try({
118
+ catch: (error) => error,
119
+ try: () => apiClients(options, dependencies)
120
+ }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
121
+ catch: (error) => error,
122
+ try: async () => await coreApi.patchNamespacedConfigMap({
123
+ body,
124
+ name,
125
+ namespace
126
+ }, setHeaderOptions("Content-Type", PatchStrategy.MergePatch))
79
127
  })))
80
128
  });
81
- function resolveKubeConfig(options, dependencies) {
82
- if (dependencies.kubeConfig) return dependencies.kubeConfig;
83
- const kubeConfig = new KubeConfig();
84
- if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
85
- else kubeConfig.loadFromDefault();
86
- if (options.kubeContext) {
87
- if (!kubeConfig.getContextObject(options.kubeContext)) throw new Error(`Kube context '${options.kubeContext}' was not found in the resolved kubeconfig`);
88
- kubeConfig.setCurrentContext(options.kubeContext);
89
- }
90
- return kubeConfig;
91
- }
92
- function apiClients(options, dependencies) {
93
- if (dependencies.coreApi && dependencies.workflowApi) return {
94
- coreApi: dependencies.coreApi,
95
- workflowApi: dependencies.workflowApi
96
- };
97
- return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
98
- }
99
- function buildApiClients(kubeConfig, dependencies) {
100
- return {
101
- coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
102
- workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
103
- };
104
- }
105
- function readApiClient(options, dependencies) {
106
- if (dependencies.workflowReadApi) return dependencies.workflowReadApi;
107
- return resolveKubeConfig(options, dependencies).makeApiClient(CustomObjectsApi);
108
- }
109
- function kubectlArgs(args, kubeContext) {
110
- return kubeContext ? [
111
- "--context",
112
- kubeContext,
113
- ...args
114
- ] : [...args];
115
- }
116
- function kubectlErrorStderr(error) {
117
- const parsed = error;
118
- return (parsed.stderr || parsed.shortMessage || "kubectl failed").trim();
119
- }
120
- function kubectlErrorStdout(error) {
121
- return (error.stdout || "").trim();
122
- }
123
129
  //#endregion
124
130
  export { KubernetesArgoService, KubernetesArgoServiceLive, resolveKubeConfig };
@@ -0,0 +1,115 @@
1
+ import { isRecord } from "../../safe-json.js";
2
+ import { numberField, stringField } from "../../unknown-fields.js";
3
+ import { resolveKubeConfig } from "./kubernetes-argo-service.js";
4
+ import { JOB_LABEL_NODE, JOB_LABEL_RUN_ID } from "../../remote/jobs/policy.js";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Option$1 from "effect/Option";
7
+ import * as R from "effect/Record";
8
+ import * as Arr from "effect/Array";
9
+ import { BatchV1Api, CoreV1Api } from "@kubernetes/client-node";
10
+ //#region src/runtime/services/kubernetes-jobs-read-service.ts
11
+ const field = (value, key) => isRecord(value) ? value[key] : void 0;
12
+ const nodeIdOf = (item) => stringField(field(field(item, "metadata"), "labels"), JOB_LABEL_NODE);
13
+ const numberOr = (value, key) => Option$1.getOrElse(numberField(value, key), () => 0);
14
+ const stringOr = (value, key) => Option$1.getOrElse(stringField(value, key), () => "");
15
+ const toCondition = (raw) => ({
16
+ ...Option$1.match(stringField(raw, "reason"), {
17
+ onNone: () => ({}),
18
+ onSome: (reason) => ({ reason })
19
+ }),
20
+ status: stringOr(raw, "status"),
21
+ type: stringOr(raw, "type")
22
+ });
23
+ const toObservedJob = (item) => {
24
+ const status = field(item, "status");
25
+ const conditions = field(status, "conditions");
26
+ return {
27
+ active: numberOr(status, "active"),
28
+ conditions: Arr.isArray(conditions) ? conditions.map(toCondition) : [],
29
+ failed: numberOr(status, "failed"),
30
+ succeeded: numberOr(status, "succeeded")
31
+ };
32
+ };
33
+ const toObservedPod = (item) => {
34
+ const status = field(item, "status");
35
+ return {
36
+ phase: stringOr(status, "phase"),
37
+ ...Option$1.match(stringField(status, "reason"), {
38
+ onNone: () => ({}),
39
+ onSome: (reason) => ({ reason })
40
+ })
41
+ };
42
+ };
43
+ const pair = (key, value) => [key, value];
44
+ const itemsOf = (list) => {
45
+ const items = field(list, "items");
46
+ return Arr.isArray(items) ? items : [];
47
+ };
48
+ const nodeEntries = (items, project) => items.flatMap((item) => Arr.fromOption(Option$1.map(nodeIdOf(item), (nodeId) => pair(nodeId, project(item)))));
49
+ /**
50
+ * Fold a run's raw `V1JobList` + `V1PodList` into the per-node observation the
51
+ * reconcile classifier consumes. Each Job/pod is keyed by its `node` label; a
52
+ * node that has pods but no Job (Job GC'd while a pod lingers) surfaces with an
53
+ * absent `job`, which the classifier treats as the dead-Job signal.
54
+ */
55
+ const parseRunObservation = (jobList, podList) => {
56
+ const jobsByNode = R.fromEntries(nodeEntries(itemsOf(jobList), toObservedJob));
57
+ const podPairs = nodeEntries(itemsOf(podList), toObservedPod);
58
+ const podsByNode = Arr.groupBy(podPairs, ([nodeId]) => nodeId);
59
+ const nodeIds = Arr.dedupe([...R.keys(jobsByNode), ...R.keys(podsByNode)]);
60
+ return { nodes: R.fromEntries(nodeIds.map((nodeId) => {
61
+ const pods = Option$1.match(R.get(podsByNode, nodeId), {
62
+ onNone: () => [],
63
+ onSome: (grouped) => grouped.map(([, pod]) => pod)
64
+ });
65
+ const observation = Option$1.match(R.get(jobsByNode, nodeId), {
66
+ onNone: () => ({ pods }),
67
+ onSome: (job) => ({
68
+ job,
69
+ pods
70
+ })
71
+ });
72
+ return pair(nodeId, observation);
73
+ })) };
74
+ };
75
+ const listApiClient = (options, dependencies) => {
76
+ if (dependencies.listApi !== void 0) return dependencies.listApi;
77
+ const kubeConfig = resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig });
78
+ const batchApi = kubeConfig.makeApiClient(BatchV1Api);
79
+ const coreApi = kubeConfig.makeApiClient(CoreV1Api);
80
+ return {
81
+ listJobs: async ({ labelSelector, namespace }) => await batchApi.listNamespacedJob({
82
+ labelSelector,
83
+ namespace
84
+ }),
85
+ listPods: async ({ labelSelector, namespace }) => await coreApi.listNamespacedPod({
86
+ labelSelector,
87
+ namespace
88
+ })
89
+ };
90
+ };
91
+ /**
92
+ * Build the {@link JobFleetReadApi} the reconcile loop reads cluster truth
93
+ * through. Lists a run's Jobs+pods by its run-id label and folds them via
94
+ * {@link parseRunObservation}. Client resolution is deferred into the Effect so
95
+ * a kubeconfig failure surfaces as a typed error, not a constructor throw.
96
+ */
97
+ const kubernetesJobFleetReadApi = (options, dependencies = {}) => ({ observeRun: ({ namespace, runId }) => Effect.try({
98
+ catch: (error) => error,
99
+ try: () => listApiClient(options, dependencies)
100
+ }).pipe(Effect.flatMap((listApi) => Effect.tryPromise({
101
+ catch: (error) => error,
102
+ try: async () => {
103
+ const labelSelector = `${JOB_LABEL_RUN_ID}=${runId}`;
104
+ const [jobList, podList] = await Promise.all([listApi.listJobs({
105
+ labelSelector,
106
+ namespace
107
+ }), listApi.listPods({
108
+ labelSelector,
109
+ namespace
110
+ })]);
111
+ return parseRunObservation(jobList, podList);
112
+ }
113
+ }))) });
114
+ //#endregion
115
+ export { kubernetesJobFleetReadApi, parseRunObservation };
@@ -0,0 +1,37 @@
1
+ import "./kubernetes-argo-service.js";
2
+ import { RunnerJobManifest } from "../../remote/jobs/model.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+ import { KubeConfig } from "@kubernetes/client-node";
5
+ //#region src/runtime/services/kubernetes-jobs-service.d.ts
6
+ /**
7
+ * Client seam for the Jobs transport (doc-1 decision 3). Deliberately narrow:
8
+ * the only cluster mutation the coordinator performs to launch a node is
9
+ * `createNamespacedJob`. There is NO ConfigMap method here — that is the
10
+ * structural guarantee behind ENG-18 AC#2 ("zero per-run ConfigMaps"): the
11
+ * Jobs path cannot create a ConfigMap because it holds no ConfigMap client.
12
+ *
13
+ * Client injection mirrors {@link KubernetesArgoService}: tests pass a fake
14
+ * `batchApi`; production resolves a real {@link BatchV1Api} from the kubeconfig.
15
+ */
16
+ interface BatchApi {
17
+ readonly createNamespacedJob: (param: {
18
+ readonly body: RunnerJobManifest;
19
+ readonly namespace: string;
20
+ }) => Promise<unknown>;
21
+ /**
22
+ * ENG-57.6: read one Job by name — the OBSERVE half of create-or-observe. When
23
+ * a deterministic `(run, node, generation)` create races (or replays) into an
24
+ * AlreadyExists, the coordinator reads the existing Job rather than creating a
25
+ * duplicate. Optional so pre-57.6 fakes/callers that never observe stay valid.
26
+ */
27
+ readonly readNamespacedJob?: (param: {
28
+ readonly name: string;
29
+ readonly namespace: string;
30
+ }) => Promise<unknown>;
31
+ }
32
+ interface KubernetesJobsIoDependencies {
33
+ batchApi?: BatchApi;
34
+ kubeConfig?: KubeConfig;
35
+ }
36
+ //#endregion
37
+ export { BatchApi, KubernetesJobsIoDependencies };
@@ -0,0 +1,45 @@
1
+ import { resolveKubeConfig } from "./kubernetes-argo-service.js";
2
+ import { Context, Effect, Layer } from "effect";
3
+ import { BatchV1Api } from "@kubernetes/client-node";
4
+ //#region src/runtime/services/kubernetes-jobs-service.ts
5
+ const batchApiClient = (options, dependencies) => {
6
+ if (dependencies.batchApi !== void 0) return dependencies.batchApi;
7
+ return resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig }).makeApiClient(BatchV1Api);
8
+ };
9
+ /** Raised when a create-or-observe read is required but the injected client has
10
+ * no read capability — a wiring error, surfaced rather than silently skipped. */
11
+ var JobReadUnsupportedError = class extends Error {
12
+ constructor() {
13
+ super("KubernetesJobsService read: injected BatchApi does not support readNamespacedJob");
14
+ this.name = "JobReadUnsupportedError";
15
+ }
16
+ };
17
+ var KubernetesJobsService = class extends Context.Service()("KubernetesJobsService") {};
18
+ const KubernetesJobsServiceLive = Layer.succeed(KubernetesJobsService, {
19
+ createJob: ({ body, dependencies, namespace, options }) => Effect.try({
20
+ catch: (error) => error,
21
+ try: () => batchApiClient(options, dependencies)
22
+ }).pipe(Effect.flatMap((batchApi) => Effect.tryPromise({
23
+ catch: (error) => error,
24
+ try: async () => await batchApi.createNamespacedJob({
25
+ body,
26
+ namespace
27
+ })
28
+ }))),
29
+ readJob: ({ dependencies, name, namespace, options }) => Effect.try({
30
+ catch: (error) => error,
31
+ try: () => batchApiClient(options, dependencies)
32
+ }).pipe(Effect.flatMap((batchApi) => {
33
+ const read = batchApi.readNamespacedJob;
34
+ if (read === void 0) return Effect.fail(new JobReadUnsupportedError());
35
+ return Effect.tryPromise({
36
+ catch: (error) => error,
37
+ try: async () => await read({
38
+ name,
39
+ namespace
40
+ })
41
+ });
42
+ }))
43
+ });
44
+ //#endregion
45
+ export { JobReadUnsupportedError, KubernetesJobsService, KubernetesJobsServiceLive };
@@ -0,0 +1,61 @@
1
+ import { resolveKubeConfig } from "./kubernetes-argo-service.js";
2
+ import { JOBS_TRANSPORT_SOURCE, JOB_LABEL_SOURCE } from "../../remote/jobs/policy.js";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Option$1 from "effect/Option";
5
+ import { Watch } from "@kubernetes/client-node";
6
+ import * as Queue from "effect/Queue";
7
+ //#region src/runtime/services/kubernetes-jobs-watch-service.ts
8
+ const watchStarterOf = (options, dependencies) => {
9
+ if (dependencies.watchStarter !== void 0) return dependencies.watchStarter;
10
+ const watch = new Watch(resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig }));
11
+ return async (path, queryParams, callback, done) => watch.watch(path, queryParams, callback, done);
12
+ };
13
+ const jobsPath = (namespace) => `/apis/batch/v1/namespaces/${namespace}/jobs`;
14
+ const podsPath = (namespace) => `/api/v1/namespaces/${namespace}/pods`;
15
+ /** Open one watch stream, bridging its `callback`/`done` onto `queue`, and
16
+ * register its `AbortController` for release on any scope exit. */
17
+ const openWatch = (starter, path, labelSelector, queue) => Effect.acquireRelease(Effect.tryPromise({
18
+ catch: (error) => error,
19
+ try: async () => starter(path, { labelSelector }, () => Effect.runSync(Queue.offer(queue, { kind: "event" })), (error) => Effect.runSync(Queue.offer(queue, {
20
+ error,
21
+ kind: "done"
22
+ })))
23
+ }), (controller) => Effect.sync(() => controller.abort()));
24
+ /** Register the caller-abort listener for release (removal) on any scope exit. */
25
+ const registerAbortListener = (signal, queue) => Option$1.match(signal, {
26
+ onNone: () => Effect.void,
27
+ onSome: (activeSignal) => {
28
+ const onAbort = () => {
29
+ Effect.runSync(Queue.offer(queue, { kind: "aborted" }));
30
+ };
31
+ return Effect.acquireRelease(Effect.sync(() => activeSignal.addEventListener("abort", onAbort, { once: true })), () => Effect.sync(() => activeSignal.removeEventListener("abort", onAbort)));
32
+ }
33
+ });
34
+ /** Drain the shared queue: run `onEvent` per event, sequenced and awaited (so a
35
+ * failing reconcile surfaces through the Effect failure channel, same as a
36
+ * transport error); stop at the first terminal message from either stream or
37
+ * the caller-abort listener. */
38
+ const drainSession = (queue, onEvent) => Effect.gen(function* drain() {
39
+ const message = yield* Queue.take(queue);
40
+ if (message.kind === "aborted") return "aborted";
41
+ if (message.kind === "done") {
42
+ if (message.error !== null && message.error !== void 0) return yield* Effect.fail(message.error);
43
+ return "closed";
44
+ }
45
+ yield* onEvent;
46
+ return yield* drainSession(queue, onEvent);
47
+ });
48
+ const kubernetesJobFleetWatchApi = (options, dependencies = {}) => ({ watchSession: ({ namespace, onEvent }) => {
49
+ if (dependencies.signal?.aborted === true) return Effect.succeed("aborted");
50
+ return Effect.scoped(Effect.gen(function* session() {
51
+ const starter = watchStarterOf(options, dependencies);
52
+ const labelSelector = `${JOB_LABEL_SOURCE}=${JOBS_TRANSPORT_SOURCE}`;
53
+ const queue = yield* Queue.unbounded();
54
+ yield* openWatch(starter, jobsPath(namespace), labelSelector, queue);
55
+ yield* openWatch(starter, podsPath(namespace), labelSelector, queue);
56
+ yield* registerAbortListener(Option$1.fromNullishOr(dependencies.signal), queue);
57
+ return yield* drainSession(queue, onEvent);
58
+ }));
59
+ } });
60
+ //#endregion
61
+ export { kubernetesJobFleetWatchApi };