@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.
- package/README.md +14 -2
- package/defaults/pipeline.yaml +23 -5
- package/defaults/profiles.yaml +104 -21
- package/dist/argo-graph.js +90 -88
- package/dist/argo-submit.d.ts +110 -138
- package/dist/argo-submit.js +363 -378
- package/dist/argo-workflow.d.ts +156 -177
- package/dist/argo-workflow.js +96 -138
- package/dist/backlog.js +5 -4
- package/dist/bench/eval-report.js +21 -12
- package/dist/claude-settings-config.js +23 -31
- package/dist/claude-user-config.js +6 -16
- package/dist/cli/bootstrap-commands.js +3 -3
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +176 -171
- package/dist/cli/factory-commands.js +13 -13
- package/dist/cli/format.js +92 -86
- package/dist/cli/loop-commands.js +21 -21
- package/dist/cli/mcp-gateway-commands.js +12 -12
- package/dist/cli/plan-commands.js +85 -116
- package/dist/cli/program.d.ts +3 -3
- package/dist/cli/program.js +41 -43
- package/dist/cli/run-command.d.ts +1 -1
- package/dist/cli/run-command.js +25 -25
- package/dist/cli/run-commands.js +31 -29
- package/dist/cli/run-resolver.d.ts +1 -1
- package/dist/cli/run-resolver.js +30 -32
- package/dist/cli/run-service.d.ts +2 -3
- package/dist/cli/run-service.js +229 -239
- package/dist/cli/submit-options.js +85 -83
- package/dist/cluster-doctor.js +147 -175
- package/dist/codex-config.js +24 -30
- package/dist/commands/bench-command.js +9 -7
- package/dist/commands/coordinator-command.js +35 -0
- package/dist/commands/pipeline-command.js +13 -7
- package/dist/commands/runner-command-command.js +39 -24
- package/dist/commands/runner-node-command.js +31 -0
- package/dist/commands/ticket/complete.js +29 -33
- package/dist/commands/ticket/create.js +84 -82
- package/dist/commands/ticket/graph-check.js +11 -11
- package/dist/commands/ticket/next.js +36 -36
- package/dist/commands/ticket/sequence.js +11 -12
- package/dist/commands/ticket/shared.d.ts +5 -2
- package/dist/commands/ticket/shared.js +63 -66
- package/dist/commands/ticket/start.js +58 -67
- package/dist/commands/ticket-command.js +2 -2
- package/dist/config/defaults.d.ts +139 -140
- package/dist/config/defaults.js +81 -80
- package/dist/config/lint.js +52 -64
- package/dist/config/load.d.ts +4 -5
- package/dist/config/load.js +64 -74
- package/dist/config/schema/catalog.d.ts +2 -1
- package/dist/config/schema/catalog.js +3 -2
- package/dist/config/schema/mcp.js +46 -52
- package/dist/config/schemas.d.ts +382 -528
- package/dist/config/schemas.js +400 -398
- package/dist/config/validate.d.ts +1 -2
- package/dist/config/validate.js +236 -201
- package/dist/config.d.ts +2 -2
- package/dist/context/repo-map.js +105 -119
- package/dist/coordinator/composition.js +169 -0
- package/dist/coordinator/config.js +71 -0
- package/dist/coordinator/run-owner.js +64 -0
- package/dist/coordinator/wave-advance.js +148 -0
- package/dist/coordinator/wave-scheduler.js +50 -0
- package/dist/credentials/broker.d.ts +6 -8
- package/dist/credentials/broker.js +17 -16
- package/dist/credentials/codex-config.js +15 -19
- package/dist/credentials/file-targets.js +9 -15
- package/dist/credentials/local-codex-auth-sync.js +55 -58
- package/dist/credentials/opencode-config.js +28 -32
- package/dist/credentials/runner.js +13 -13
- package/dist/effect-sync-errors.js +28 -0
- package/dist/factory/create-experiment.js +285 -121
- package/dist/factory/exec.js +10 -12
- package/dist/factory/git-credentials.js +4 -4
- package/dist/factory/stamp-answers.js +10 -11
- package/dist/factory/template-update.js +65 -44
- package/dist/factory-lane.d.ts +26 -40
- package/dist/factory-lane.js +26 -25
- package/dist/gates.js +3 -3
- package/dist/git-remote-url.js +17 -19
- package/dist/hooks.d.ts +26 -26
- package/dist/hooks.js +20 -22
- package/dist/index.d.ts +4 -3
- package/dist/index.js +27 -36
- package/dist/install-commands/claude-code.js +100 -92
- package/dist/install-commands/host-selection.js +3 -6
- package/dist/install-commands/opencode.js +264 -259
- package/dist/install-commands/planner.js +163 -181
- package/dist/install-commands/result-format.js +1 -3
- package/dist/install-commands/shared.js +25 -42
- package/dist/install-commands/writer.js +22 -25
- package/dist/install-commands.js +8 -8
- package/dist/jscpd-output.js +29 -0
- package/dist/json-config-merge.js +14 -20
- package/dist/json-line-values.js +17 -6
- package/dist/loop/argo-poll.d.ts +0 -1
- package/dist/loop/argo-poll.js +15 -95
- package/dist/loop/backlog-records.js +1 -3
- package/dist/loop/controller-deps.js +150 -161
- package/dist/loop/controller.js +164 -186
- package/dist/loop/gh-checks.js +62 -61
- package/dist/loop/gh-runner.js +22 -25
- package/dist/loop/loop-command.js +66 -58
- package/dist/loop/loop-controller-entrypoint.js +29 -30
- package/dist/loop/merge.js +36 -40
- package/dist/loop/run-terminal-poll.js +69 -0
- package/dist/mcp/gateway-config.js +25 -29
- package/dist/mcp/gateway-doctor.js +77 -119
- package/dist/mcp/gateway-reconcile.js +17 -22
- package/dist/mcp/gateway-runtime.js +1 -3
- package/dist/mcp/host-config.js +37 -40
- package/dist/mcp/host-renderers.js +10 -18
- package/dist/mcp/repo-local-backends.js +51 -54
- package/dist/mcp/toolhive-vmcp.js +39 -39
- package/dist/model-resolver.js +29 -33
- package/dist/moka-global-config.d.ts +78 -55
- package/dist/moka-global-config.js +93 -92
- package/dist/moka-submit.d.ts +2179 -866
- package/dist/moka-submit.js +97 -99
- package/dist/opencode-project-config.js +34 -40
- package/dist/package-assets.js +1 -3
- package/dist/path-refs.js +2 -2
- package/dist/pipeline-init.js +25 -25
- package/dist/pipeline-runtime.d.ts +15 -14
- package/dist/pipeline-runtime.js +51 -50
- package/dist/planned-node.js +6 -3
- package/dist/planning/compile.d.ts +5 -5
- package/dist/planning/compile.js +140 -139
- package/dist/planning/dependency-refs.js +5 -5
- package/dist/planning/generate.d.ts +1296 -1296
- package/dist/planning/generate.js +307 -314
- package/dist/planning/graph.d.ts +0 -1
- package/dist/planning/graph.js +131 -149
- package/dist/remote/argo/model.d.ts +104 -122
- package/dist/remote/argo/model.js +205 -197
- package/dist/remote/argo/policy.js +39 -47
- package/dist/remote/argo/storage.js +81 -85
- package/dist/remote/jobs/execution-identity.js +70 -0
- package/dist/remote/jobs/job-outcome.js +108 -0
- package/dist/remote/jobs/manifest.js +96 -0
- package/dist/remote/jobs/model.d.ts +158 -0
- package/dist/remote/jobs/model.js +162 -0
- package/dist/remote/jobs/node-bootstrap.js +213 -0
- package/dist/remote/jobs/policy.js +123 -0
- package/dist/remote/jobs/reconcile.js +100 -0
- package/dist/remote/jobs/run-spec-client.js +64 -0
- package/dist/remote/jobs/run-spec-path.js +31 -0
- package/dist/remote/jobs/run-spec-server.d.ts +34 -0
- package/dist/remote/jobs/run-spec-server.js +116 -0
- package/dist/remote/jobs/submit.d.ts +23 -0
- package/dist/remote/jobs/submit.js +116 -0
- package/dist/remote/jobs/watch.js +59 -0
- package/dist/remote/submit/argo-submission.d.ts +3 -1
- package/dist/remote/submit/argo-submission.js +53 -58
- package/dist/remote/submit/compilation.d.ts +2 -0
- package/dist/remote/submit/compilation.js +46 -49
- package/dist/remote/submit/event-boundary.js +62 -76
- package/dist/remote/submit/hook-events.d.ts +6 -0
- package/dist/remote/submit/io.d.ts +7 -1
- package/dist/remote/submit/io.js +40 -43
- package/dist/remote/submit/jobs-submission.d.ts +59 -0
- package/dist/remote/submit/jobs-submission.js +97 -0
- package/dist/remote/submit/jobs-submit-result.d.ts +25 -0
- package/dist/remote/submit/jobs-submit-result.js +34 -0
- package/dist/remote/submit/options.d.ts +49 -0
- package/dist/remote/submit/options.js +27 -0
- package/dist/remote/submit/service.d.ts +40 -1
- package/dist/remote/submit/service.js +67 -49
- package/dist/run-control/command-context.js +6 -8
- package/dist/run-control/commands.js +10 -10
- package/dist/run-control/contracts.d.ts +52 -101
- package/dist/run-control/contracts.js +83 -69
- package/dist/run-control/detach.js +66 -72
- package/dist/run-control/execution-store.js +0 -0
- package/dist/run-control/file-errors.js +1 -3
- package/dist/run-control/logical-segment.js +3 -3
- package/dist/run-control/next-node.js +67 -78
- package/dist/run-control/postgres/postgres-run-control-store.js +196 -225
- package/dist/run-control/resume-command.js +34 -41
- package/dist/run-control/run-artifacts-command.js +84 -105
- package/dist/run-control/run-command-domain.js +7 -11
- package/dist/run-control/run-control-store.js +9 -9
- package/dist/run-control/run-query-command.js +73 -100
- package/dist/run-control/run-record.js +5 -3
- package/dist/run-control/run-state-lock.js +5 -5
- package/dist/run-control/runtime-event-projection.js +38 -50
- package/dist/run-control/runtime-reporter.js +86 -102
- package/dist/run-control/stop-command.js +42 -53
- package/dist/run-control/store-fs-effects.js +3 -1
- package/dist/run-control/store-manifest.js +38 -13
- package/dist/run-control/store-paths.js +3 -3
- package/dist/run-control/store.js +1 -1
- package/dist/run-control/submit-result.js +21 -21
- package/dist/run-control/supervisor.js +78 -68
- package/dist/run-control/workspace.js +2 -2
- package/dist/run-state/git-refs.js +258 -307
- package/dist/runner/opencode-excludes.js +4 -4
- package/dist/runner/subprocess-result.js +28 -37
- package/dist/runner/subprocess.js +41 -41
- package/dist/runner/timeouts.js +14 -13
- package/dist/runner-command/dynamic-command.js +46 -39
- package/dist/runner-command/environment-commands.js +91 -0
- package/dist/runner-command/finalize.js +174 -153
- package/dist/runner-command/lifecycle-context.js +115 -130
- package/dist/runner-command/lifecycle.js +78 -72
- package/dist/runner-command/pre-schedule.js +203 -223
- package/dist/runner-command/run.js +450 -448
- package/dist/runner-command/schedule-source-options.js +8 -10
- package/dist/runner-command/select-ready-wave.js +39 -30
- package/dist/runner-command/task-descriptor.js +11 -17
- package/dist/runner-command-contract.d.ts +147 -157
- package/dist/runner-command-contract.js +403 -411
- package/dist/runner-event-schema.d.ts +364 -505
- package/dist/runner-event-schema.js +187 -175
- package/dist/runner-event-sink.js +37 -43
- package/dist/runner-output.js +6 -6
- package/dist/runner.d.ts +57 -37
- package/dist/runner.js +176 -127
- package/dist/runtime/actor-ids.d.ts +1 -0
- package/dist/runtime/actor-ids.js +2 -2
- package/dist/runtime/agent-node/agent-node.js +99 -110
- package/dist/runtime/agent-node/handoff-finalization.js +23 -24
- package/dist/runtime/agent-node/model-selection.js +19 -22
- package/dist/runtime/agent-node/output-finalization.js +174 -204
- package/dist/runtime/agent-node/prompt-rendering.js +208 -251
- package/dist/runtime/agent-node/session-execution.js +43 -39
- package/dist/runtime/builtins/builtins.js +218 -284
- package/dist/runtime/changed-files/changed-files.js +29 -34
- package/dist/runtime/command-executor/command-executor.js +31 -23
- package/dist/runtime/config-error.js +1 -3
- package/dist/runtime/context/context.js +35 -37
- package/dist/runtime/contracts/contracts.d.ts +3 -0
- package/dist/runtime/contracts/index.d.ts +1 -1
- package/dist/runtime/detached-race.js +11 -14
- package/dist/runtime/drain-merge/drain-merge.js +139 -164
- package/dist/runtime/durable-store/acquisition.js +5 -3
- package/dist/runtime/durable-store/durable-store.d.ts +3 -1
- package/dist/runtime/durable-store/postgres/client.js +29 -0
- package/dist/runtime/durable-store/postgres/migrate-substrate.js +34 -29
- package/dist/runtime/durable-store/postgres/postgres-store.js +18 -20
- package/dist/runtime/events/events.js +273 -331
- package/dist/runtime/events/index.js +2 -2
- package/dist/runtime/execution-envelope.js +44 -0
- package/dist/runtime/gates/adjudication/llm-judge.js +21 -24
- package/dist/runtime/gates/adjudication/structured-claim.js +15 -16
- package/dist/runtime/gates/adjudicator/adjudicator.js +27 -27
- package/dist/runtime/gates/adjudicator/index.js +2 -2
- package/dist/runtime/gates/contract.js +5 -9
- package/dist/runtime/gates/gates.js +9 -8
- package/dist/runtime/gates/kinds/acceptance/acceptance.js +68 -71
- package/dist/runtime/gates/kinds/acceptance/index.js +2 -2
- package/dist/runtime/gates/kinds/artifact/artifact.js +4 -4
- package/dist/runtime/gates/kinds/artifact/index.js +2 -2
- package/dist/runtime/gates/kinds/builtin/builtin.js +4 -4
- package/dist/runtime/gates/kinds/builtin/index.js +2 -2
- package/dist/runtime/gates/kinds/changed-files/changed-files.js +31 -35
- package/dist/runtime/gates/kinds/changed-files/index.js +2 -2
- package/dist/runtime/gates/kinds/command/command.js +3 -3
- package/dist/runtime/gates/kinds/command/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/json-schema.js +7 -6
- package/dist/runtime/gates/kinds/verdict/index.js +2 -2
- package/dist/runtime/gates/kinds/verdict/verdict.js +3 -3
- package/dist/runtime/gates/orchestrator.js +72 -86
- package/dist/runtime/gates/registry.js +1 -3
- package/dist/runtime/handoff.d.ts +2 -1
- package/dist/runtime/handoff.js +43 -46
- package/dist/runtime/hooks/command-hook.js +57 -71
- package/dist/runtime/hooks/context.js +21 -25
- package/dist/runtime/hooks/events.js +84 -100
- package/dist/runtime/hooks/execution.js +6 -12
- package/dist/runtime/hooks/hooks.js +34 -23
- package/dist/runtime/hooks/invocation.js +23 -29
- package/dist/runtime/hooks/module-hook.js +36 -35
- package/dist/runtime/hooks/policy.js +32 -29
- package/dist/runtime/hooks/results.js +56 -66
- package/dist/runtime/journal-acquisition.d.ts +3 -2
- package/dist/runtime/journal-acquisition.js +14 -14
- package/dist/runtime/json-validation/json-validation.js +51 -72
- package/dist/runtime/launch-plan-args.js +39 -0
- package/dist/runtime/local-scheduler.js +46 -48
- package/dist/runtime/node-execution.js +376 -445
- package/dist/runtime/node-protocol/node-protocol.js +36 -79
- package/dist/runtime/node-state-store.d.ts +4 -1
- package/dist/runtime/node-state-store.js +13 -57
- package/dist/runtime/node-state-tracker.js +46 -54
- package/dist/runtime/open-pull-request/open-pull-request.js +201 -187
- package/dist/runtime/opencode-adapter.js +23 -24
- package/dist/runtime/opencode-agent-name.js +6 -6
- package/dist/runtime/opencode-runtime.js +115 -93
- package/dist/runtime/opencode-server.js +39 -53
- package/dist/runtime/opencode-session-executor.js +312 -344
- package/dist/runtime/parallel-node/parallel-node.js +123 -134
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +90 -73
- package/dist/runtime/protected-paths/protected-paths.js +70 -71
- package/dist/runtime/remediation/remediation.js +202 -231
- package/dist/runtime/retry.js +8 -10
- package/dist/runtime/run-journal.d.ts +2 -0
- package/dist/runtime/run-journal.js +7 -7
- package/dist/runtime/runtime-results.js +54 -69
- package/dist/runtime/scheduled-dependencies.d.ts +2 -1
- package/dist/runtime/scheduled-dependencies.js +61 -75
- package/dist/runtime/scheduler.js +179 -202
- package/dist/runtime/services/agent-node-runtime-service.js +3 -3
- package/dist/runtime/services/backlog-service.d.ts +1 -2
- package/dist/runtime/services/backlog-service.js +8 -14
- package/dist/runtime/services/command-executor-service.js +1 -1
- package/dist/runtime/services/config-io-service.js +24 -21
- package/dist/runtime/services/drain-merge-git-service.js +1 -1
- package/dist/runtime/services/file-system-service.js +9 -7
- package/dist/runtime/services/git-porcelain-service.js +6 -6
- package/dist/runtime/services/kubernetes-argo-service.d.ts +10 -3
- package/dist/runtime/services/kubernetes-argo-service.js +90 -84
- package/dist/runtime/services/kubernetes-jobs-read-service.js +115 -0
- package/dist/runtime/services/kubernetes-jobs-service.d.ts +37 -0
- package/dist/runtime/services/kubernetes-jobs-service.js +45 -0
- package/dist/runtime/services/kubernetes-jobs-watch-service.js +61 -0
- package/dist/runtime/services/kubernetes-lease-service.js +21 -0
- package/dist/runtime/services/mcp-gateway-service.js +143 -94
- package/dist/runtime/services/open-pull-request-git-service.js +1 -3
- package/dist/runtime/services/opencode-runtime-server-service.js +10 -12
- package/dist/runtime/services/opencode-sdk-service.js +7 -7
- package/dist/runtime/services/repo-io-service.js +10 -12
- package/dist/runtime/services/run-journal-file-service.js +2 -2
- package/dist/runtime/services/runner-command-io-service.js +48 -52
- package/dist/runtime/services/runner-event-sink-http-service.js +32 -39
- package/dist/runtime/step/step-node.js +15 -15
- package/dist/runtime/workflow-execution.js +56 -57
- package/dist/runtime/workflow-lifecycle.js +82 -101
- package/dist/runtime/yeet-event-record.js +104 -0
- package/dist/runtime/yeet-executor.js +184 -0
- package/dist/safe-json.js +16 -16
- package/dist/schedule/backlog-context.js +122 -116
- package/dist/schedule/baseline.js +249 -255
- package/dist/schedule/passes/coverage.js +63 -68
- package/dist/schedule/passes/drain-merge.js +34 -40
- package/dist/schedule/passes/ids.js +15 -17
- package/dist/schedule/passes/models.js +40 -37
- package/dist/schedule/passes/open-pull-request.js +16 -20
- package/dist/schedule/passes/references.js +18 -22
- package/dist/schedule/prompts.js +135 -141
- package/dist/schedule/scheduling-roles.js +8 -14
- package/dist/schema-boundary.d.ts +4 -0
- package/dist/schema-boundary.js +145 -0
- package/dist/serialized-write-queue.js +2 -2
- package/dist/standard-output-schemas.js +61 -59
- package/dist/strings.js +12 -12
- package/dist/task-ref.js +5 -5
- package/dist/tickets/apply-ticket-plan.js +52 -61
- package/dist/tickets/backlog-task-store.d.ts +15 -23
- package/dist/tickets/backlog-task-store.js +82 -94
- package/dist/tickets/completion/complete-ticket.js +53 -59
- package/dist/tickets/ticket-graph-dto.d.ts +24 -48
- package/dist/tickets/ticket-graph-dto.js +40 -40
- package/dist/tickets/ticket-graph.d.ts +7 -2
- package/dist/tickets/ticket-graph.js +36 -50
- package/dist/tickets/ticket-plan-command-args.js +6 -10
- package/dist/tickets/ticket-plan-render.js +10 -16
- package/dist/tickets/ticket-plan.d.ts +32 -41
- package/dist/tickets/ticket-plan.js +97 -77
- package/dist/tickets/ticket-selection.d.ts +3 -1
- package/dist/tickets/ticket-selection.js +37 -52
- package/dist/tickets/ticket-task-index.js +13 -17
- package/dist/tickets/validation-error-format.js +3 -10
- package/dist/token-estimator.js +12 -7
- package/dist/unknown-fields.js +21 -0
- package/dist/workflow-submit-contract.d.ts +9 -10
- package/dist/workflow-submit-contract.js +10 -9
- package/docs/adr-moka-postgres-schema-isolation.md +48 -0
- package/docs/adr-opencode-first-goal-loop-runtime.md +11 -11
- package/docs/config-architecture.md +13 -13
- package/docs/mcp-host-isolation.md +1 -0
- package/docs/moka-orchestrator-design.md +3 -3
- package/docs/operator-guide.md +1 -1
- package/docs/pipeline-console-runner-contract.md +10 -6
- package/docs/runtime-actor-model.md +18 -18
- package/docs/runtime-test-subsystems.md +200 -0
- package/docs/slash-command-adapter-contract.md +3 -3
- package/package.json +49 -85
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
80
|
-
if (
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 (
|
|
143
|
-
cleanup: () =>
|
|
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: () =>
|
|
92
|
+
cleanup: () => {},
|
|
150
93
|
controller
|
|
151
94
|
};
|
|
152
95
|
}
|
|
153
|
-
const abort = () =>
|
|
96
|
+
const abort = () => {
|
|
97
|
+
controller.abort();
|
|
98
|
+
};
|
|
154
99
|
signal.addEventListener("abort", abort, { once: true });
|
|
155
100
|
return {
|
|
156
|
-
cleanup: () =>
|
|
101
|
+
cleanup: () => {
|
|
102
|
+
signal.removeEventListener("abort", abort);
|
|
103
|
+
},
|
|
157
104
|
controller
|
|
158
105
|
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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
|
-
|
|
153
|
+
const gcParallelWorktrees = (repoRoot) => {
|
|
137
154
|
const registryAbs = join(repoRoot, REGISTRY_DIR);
|
|
138
155
|
if (!existsSync(registryAbs)) return [];
|
|
139
|
-
const results = readdirSync(registryAbs).
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|