@oisincoveney/pipeline 2.8.0 → 2.8.2

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 (38) hide show
  1. package/dist/argo-submit.d.ts +2 -4
  2. package/dist/argo-submit.js +80 -80
  3. package/dist/cluster-doctor.js +89 -101
  4. package/dist/config/defaults.js +9 -19
  5. package/dist/config/load.js +32 -39
  6. package/dist/config/schemas.d.ts +1 -1
  7. package/dist/gates.js +6 -225
  8. package/dist/mcp/gateway-error.js +15 -0
  9. package/dist/mcp/gateway.js +119 -220
  10. package/dist/moka-global-config.js +20 -20
  11. package/dist/moka-submit.d.ts +1 -1
  12. package/dist/pipeline-runtime.js +580 -371
  13. package/dist/run-state/git-refs.js +124 -94
  14. package/dist/runner-command-contract.d.ts +2 -2
  15. package/dist/runner-event-sink.js +37 -69
  16. package/dist/runtime/agent-node/agent-node.js +214 -173
  17. package/dist/runtime/builtins/builtins.js +344 -57
  18. package/dist/runtime/changed-files/changed-files.js +15 -27
  19. package/dist/runtime/changed-files/index.js +2 -0
  20. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  21. package/dist/runtime/gates/gates.js +46 -28
  22. package/dist/runtime/hooks/hooks.js +74 -29
  23. package/dist/runtime/opencode-server.js +27 -21
  24. package/dist/runtime/opencode-session-executor.js +101 -44
  25. package/dist/runtime/parallel-node/parallel-node.js +24 -5
  26. package/dist/runtime/select-candidate/select-candidate.js +45 -29
  27. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  28. package/dist/runtime/services/command-executor-service.js +8 -0
  29. package/dist/runtime/services/config-io-service.js +42 -0
  30. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  31. package/dist/runtime/services/git-porcelain-service.js +38 -0
  32. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  33. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  34. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  35. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  36. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  37. package/dist/runtime/services/select-candidate-service.js +13 -0
  38. package/package.json +1 -1
@@ -2,12 +2,16 @@ import { parseJsonObject } from "../json-validation/json-validation.js";
2
2
  import "../json-validation/index.js";
3
3
  import { generateRuntimeRunId } from "../context/context.js";
4
4
  import "../context/index.js";
5
- import simpleGit$1 from "simple-git";
5
+ import { DrainMergeGitService, DrainMergeGitServiceLive } from "../services/drain-merge-git-service.js";
6
+ import { Effect } from "effect";
6
7
  //#region src/runtime/drain-merge/drain-merge.ts
7
8
  const LINE_RE = /\r?\n/;
8
- async function executeDrainMergeBuiltin(context, node) {
9
- const upstreamNodeId = node?.needs.at(0) ?? null;
10
- const integrationBranch = `runs/integration/${context.runId ?? generateRuntimeRunId()}`;
9
+ function executeDrainMergeBuiltin(context, node) {
10
+ return Effect.runPromise(Effect.provide(drainMergeProgram(context, node), DrainMergeGitServiceLive));
11
+ }
12
+ function drainMergeProgram(context, node) {
13
+ const upstreamNodeId = firstNeededNode(node);
14
+ const integrationBranch = `runs/integration/${runIdForDrainMerge(context)}`;
11
15
  const report = {
12
16
  baseSha: null,
13
17
  conflicts: [],
@@ -16,47 +20,31 @@ async function executeDrainMergeBuiltin(context, node) {
16
20
  skipped: []
17
21
  };
18
22
  const mergeable = drainMergeMergeableChildren(drainMergeChildren(context, upstreamNodeId), report);
19
- if (mergeable.length === 0) return drainMergeResult(report);
20
- report.baseSha = mergeable[0].output.baseSha;
21
- const divergent = mergeable.find((child) => child.output.baseSha !== report.baseSha);
22
- if (divergent) return drainMergeResult(report, {
23
+ if (mergeable.length === 0) return Effect.succeed(drainMergeResult(report));
24
+ const baseSha = mergeable[0].output.baseSha;
25
+ report.baseSha = baseSha;
26
+ const divergent = divergentDrainMergeChild(mergeable, baseSha);
27
+ if (divergent) return Effect.succeed(drainMergeResult(report, {
23
28
  evidence: [`drain-merge child '${divergent.nodeId}' baseSha ${divergent.output.baseSha} diverges from ${report.baseSha}`],
24
29
  failed: true
25
- });
26
- const git = simpleGit$1({ baseDir: context.worktreePath });
27
- try {
28
- await checkoutDrainMergeIntegrationBranch(git, integrationBranch, report.baseSha);
29
- } catch (error) {
30
- return drainMergeResult(report, {
31
- evidence: [`drain-merge setup-error: ${error instanceof Error ? error.message : String(error)}`],
32
- failed: true
33
- });
34
- }
35
- for (const child of mergeable) try {
36
- await git.raw([
37
- "merge",
38
- "--no-ff",
39
- "--no-edit",
40
- "-m",
41
- "drain-merge: merge",
42
- child.output.branch
43
- ]);
44
- report.merged.push({
45
- branch: child.output.branch,
46
- id: child.nodeId,
47
- worktreePath: child.output.worktreePath
30
+ }));
31
+ return Effect.gen(function* () {
32
+ const git = yield* (yield* DrainMergeGitService).create(context.worktreePath);
33
+ const setup = checkoutDrainMergeIntegrationBranch(git, integrationBranch, baseSha);
34
+ const setupResult = yield* Effect.either(setup);
35
+ if (setupResult._tag === "Left") return drainMergeSetupErrorResult(report, setupResult.left);
36
+ yield* Effect.forEach(mergeable, (child) => mergeDrainMergeChild(git, report, child), {
37
+ concurrency: 1,
38
+ discard: true
48
39
  });
49
- } catch {
50
- const files = await drainMergeConflictFiles(git);
51
- report.conflicts.push({
52
- branch: child.output.branch,
53
- files,
54
- id: child.nodeId,
55
- worktreePath: child.output.worktreePath
56
- });
57
- await abortDrainMerge(git);
58
- }
59
- return drainMergeResult(report);
40
+ return drainMergeResult(report);
41
+ });
42
+ }
43
+ function firstNeededNode(node) {
44
+ return node?.needs.at(0) ?? null;
45
+ }
46
+ function runIdForDrainMerge(context) {
47
+ return context.runId ?? generateRuntimeRunId();
60
48
  }
61
49
  function drainMergeChildren(context, upstreamNodeId) {
62
50
  if (!upstreamNodeId) return [];
@@ -80,7 +68,7 @@ function drainMergeMergeableChildren(children, report) {
80
68
  });
81
69
  return [];
82
70
  }
83
- if (!(child.output.baseSha && child.output.branch && child.output.worktreePath)) {
71
+ if (!hasDrainMergeWorktree(child.output)) {
84
72
  report.skipped.push({
85
73
  id: child.nodeId,
86
74
  reason: "no-worktree",
@@ -99,56 +87,110 @@ function drainMergeMergeableChildren(children, report) {
99
87
  }];
100
88
  });
101
89
  }
102
- async function checkoutDrainMergeIntegrationBranch(git, integrationBranch, baseSha) {
103
- try {
104
- await git.raw([
105
- "rev-parse",
106
- "--verify",
107
- integrationBranch
108
- ]);
109
- await git.raw(["checkout", integrationBranch]);
110
- } catch {
111
- await git.raw([
112
- "checkout",
113
- "-b",
114
- integrationBranch,
115
- baseSha
116
- ]);
117
- }
118
- }
119
- async function drainMergeConflictFiles(git) {
120
- try {
121
- return (await git.raw([
122
- "diff",
123
- "--name-only",
124
- "--diff-filter=U"
125
- ])).split(LINE_RE).filter(Boolean);
126
- } catch {
127
- return [];
128
- }
129
- }
130
- async function abortDrainMerge(git) {
131
- try {
132
- await git.raw(["merge", "--abort"]);
133
- } catch {}
90
+ function hasDrainMergeWorktree(output) {
91
+ return Boolean(output.baseSha) && Boolean(output.branch) && Boolean(output.worktreePath);
92
+ }
93
+ function divergentDrainMergeChild(mergeable, baseSha) {
94
+ return mergeable.find((child) => child.output.baseSha !== baseSha);
95
+ }
96
+ function checkoutDrainMergeIntegrationBranch(git, integrationBranch, baseSha) {
97
+ return git.raw([
98
+ "rev-parse",
99
+ "--verify",
100
+ integrationBranch
101
+ ]).pipe(Effect.flatMap(() => git.raw(["checkout", integrationBranch])), Effect.catchAll(() => git.raw([
102
+ "checkout",
103
+ "-b",
104
+ integrationBranch,
105
+ baseSha
106
+ ])), Effect.asVoid);
107
+ }
108
+ function mergeDrainMergeChild(git, report, child) {
109
+ return drainMergeChild(git, child.output.branch).pipe(Effect.tap(() => Effect.sync(() => recordDrainMergeSuccess(report, child))), Effect.catchAll(() => recordDrainMergeConflict(git, report, child)));
110
+ }
111
+ function drainMergeChild(git, branch) {
112
+ return git.raw([
113
+ "merge",
114
+ "--no-ff",
115
+ "--no-edit",
116
+ "-m",
117
+ "drain-merge: merge",
118
+ branch
119
+ ]);
120
+ }
121
+ function recordDrainMergeSuccess(report, child) {
122
+ report.merged.push({
123
+ branch: child.output.branch,
124
+ id: child.nodeId,
125
+ worktreePath: child.output.worktreePath
126
+ });
127
+ }
128
+ function recordDrainMergeConflict(git, report, child) {
129
+ return Effect.gen(function* () {
130
+ const files = yield* drainMergeConflictFiles(git);
131
+ report.conflicts.push({
132
+ branch: child.output.branch,
133
+ files,
134
+ id: child.nodeId,
135
+ worktreePath: child.output.worktreePath
136
+ });
137
+ yield* abortDrainMerge(git);
138
+ });
139
+ }
140
+ function drainMergeConflictFiles(git) {
141
+ return git.raw([
142
+ "diff",
143
+ "--name-only",
144
+ "--diff-filter=U"
145
+ ]).pipe(Effect.map((output) => output.split(LINE_RE).filter(Boolean)), Effect.catchAll(() => Effect.succeed([])));
146
+ }
147
+ function abortDrainMerge(git) {
148
+ return git.raw(["merge", "--abort"]).pipe(Effect.catchAll(() => Effect.void), Effect.asVoid);
134
149
  }
135
150
  function parseDrainMergeChildOutput(value) {
136
151
  const output = parseJsonObject(value);
137
152
  if (Object.keys(output).length === 0) return null;
138
153
  return {
139
- baseSha: typeof output.baseSha === "string" ? output.baseSha : null,
140
- branch: typeof output.branch === "string" ? output.branch : null,
141
- status: output.status === "PASS" ? "PASS" : "FAIL",
142
- worktreePath: typeof output.worktreePath === "string" ? output.worktreePath : null
154
+ baseSha: stringFieldOrNull(output.baseSha),
155
+ branch: stringFieldOrNull(output.branch),
156
+ status: drainMergeStatus(output.status),
157
+ worktreePath: stringFieldOrNull(output.worktreePath)
143
158
  };
144
159
  }
160
+ function stringFieldOrNull(value) {
161
+ return typeof value === "string" ? value : null;
162
+ }
163
+ function drainMergeStatus(value) {
164
+ return value === "PASS" ? "PASS" : "FAIL";
165
+ }
145
166
  function drainMergeResult(report, options = {}) {
146
- const hasFailure = report.conflicts.length > 0 || options.failed === true;
167
+ const hasFailure = hasDrainMergeFailure(report, options.failed);
147
168
  return {
148
- evidence: [...options.evidence ?? [], hasFailure ? `drain-merge completed with ${report.conflicts.length} conflicts` : `drain-merge merged ${report.merged.length} branches`],
149
- exitCode: hasFailure ? 1 : 0,
169
+ evidence: drainMergeEvidence(report, options.evidence, hasFailure),
170
+ exitCode: drainMergeExitCode(hasFailure),
150
171
  output: JSON.stringify(report)
151
172
  };
152
173
  }
174
+ function hasDrainMergeFailure(report, failed) {
175
+ return report.conflicts.length > 0 || failed === true;
176
+ }
177
+ function drainMergeEvidence(report, evidence, hasFailure) {
178
+ return [...evidence ?? [], drainMergeSummary(report, hasFailure)];
179
+ }
180
+ function drainMergeSummary(report, hasFailure) {
181
+ return hasFailure ? `drain-merge completed with ${report.conflicts.length} conflicts` : `drain-merge merged ${report.merged.length} branches`;
182
+ }
183
+ function drainMergeExitCode(hasFailure) {
184
+ return hasFailure ? 1 : 0;
185
+ }
186
+ function drainMergeSetupErrorResult(report, error) {
187
+ return drainMergeResult(report, {
188
+ evidence: [`drain-merge setup-error: ${errorMessage(error)}`],
189
+ failed: true
190
+ });
191
+ }
192
+ function errorMessage(error) {
193
+ return error instanceof Error ? error.message : String(error);
194
+ }
153
195
  //#endregion
154
196
  export { executeDrainMergeBuiltin };
@@ -1,46 +1,64 @@
1
1
  import { isRecord, parseJsonResult } from "../../safe-json.js";
2
2
  import { runtimeActorId } from "../actor-ids.js";
3
- import { executeCommand } from "../command-executor/command-executor.js";
4
- import "../command-executor/index.js";
5
3
  import { readOptionalFile, validateJsonSchemaSource } from "../json-validation/json-validation.js";
6
4
  import "../json-validation/index.js";
7
5
  import { emitGateFinish, emitGateStart, runtimeSystemId } from "../events/events.js";
8
6
  import "../events/index.js";
9
- import { artifactExists } from "../../gates.js";
7
+ import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
10
8
  import { executeBuiltin } from "../builtins/builtins.js";
11
9
  import "../builtins/index.js";
10
+ import { artifactExists } from "../../gates.js";
11
+ import { Effect } from "effect";
12
12
  import { join } from "node:path";
13
13
  import micromatch from "micromatch";
14
14
  //#region src/runtime/gates/gates.ts
15
- async function evaluateNodeGates(node, context, attempt, onGateFailure) {
15
+ function evaluateNodeGates(node, context, attempt, onGateFailure) {
16
+ return Effect.runPromise(Effect.provide(evaluateNodeGatesEffect(node, context, attempt, onGateFailure), CommandExecutorLive));
17
+ }
18
+ function evaluateNodeGatesEffect(node, context, attempt, onGateFailure) {
19
+ return Effect.gen(function* () {
20
+ const executor = yield* CommandExecutor;
21
+ return yield* Effect.tryPromise(() => evaluateNodeGatesWithExecutor(node, context, attempt, executor, onGateFailure));
22
+ });
23
+ }
24
+ async function evaluateNodeGatesWithExecutor(node, context, attempt, executor, onGateFailure) {
16
25
  const results = [];
17
- for (const gate of nodeGateSpecs(node, context)) {
18
- const gateId = gate.id ?? `${gate.kind}:${node.id}`;
19
- if (isCancelled(context)) {
20
- emitRuntimeGateCancelled(context, gate, gateId, node.id, "gate cancelled");
21
- break;
22
- }
23
- emitGateStart(context, node.id, gate, gateId);
24
- const result = await runGateEvaluation(gate, gateId, node.id, context, attempt);
25
- context.gates.push(result);
26
- results.push(result);
27
- emitGateFinish(context, gate, result);
28
- if (!result.passed) {
29
- await onGateFailure?.(node, result);
30
- if (gate.required !== false) break;
31
- }
32
- }
26
+ for (const gate of nodeGateSpecs(node, context)) if (await evaluateNodeGateIteration(gate, node, context, attempt, executor, results, onGateFailure) === "stop") break;
33
27
  return results;
34
28
  }
35
- async function runGateEvaluation(gate, gateId, nodeId, context, attempt) {
29
+ async function evaluateNodeGateIteration(gate, node, context, attempt, executor, results, onGateFailure) {
30
+ const gateId = gate.id ?? `${gate.kind}:${node.id}`;
31
+ if (isCancelled(context)) {
32
+ emitRuntimeGateCancelled(context, gate, gateId, node.id, "gate cancelled");
33
+ return "stop";
34
+ }
35
+ const result = await runObservedGate(gate, gateId, node.id, context, attempt, executor);
36
+ recordGateResult(context, gate, result, results);
37
+ return handleGateFailure(gate, node, result, onGateFailure);
38
+ }
39
+ function runObservedGate(gate, gateId, nodeId, context, attempt, executor) {
40
+ emitGateStart(context, nodeId, gate, gateId);
41
+ return runGateEvaluation(gate, gateId, nodeId, context, attempt, executor);
42
+ }
43
+ function recordGateResult(context, gate, result, results) {
44
+ context.gates.push(result);
45
+ results.push(result);
46
+ emitGateFinish(context, gate, result);
47
+ }
48
+ async function handleGateFailure(gate, node, result, onGateFailure) {
49
+ if (result.passed) return "continue";
50
+ if (onGateFailure) await onGateFailure(node, result);
51
+ return gate.required === false ? "continue" : "stop";
52
+ }
53
+ async function runGateEvaluation(gate, gateId, nodeId, context, attempt, executor) {
36
54
  emitRuntimeGateStarted(context, gate, gateId, nodeId);
37
- const result = await resolveGateResult(gate, gateId, nodeId, context, attempt);
55
+ const result = await resolveGateResult(gate, gateId, nodeId, context, attempt, executor);
38
56
  emitRuntimeGateResult(context, result);
39
57
  return result;
40
58
  }
41
- async function resolveGateResult(gate, gateId, nodeId, context, attempt) {
59
+ async function resolveGateResult(gate, gateId, nodeId, context, attempt, executor) {
42
60
  try {
43
- return await evaluateGate(gate, nodeId, context, attempt);
61
+ return await evaluateGate(gate, nodeId, context, attempt, executor);
44
62
  } catch (err) {
45
63
  return {
46
64
  evidence: [err instanceof Error ? err.message : String(err)],
@@ -135,11 +153,11 @@ function schemaGateSpecs(node, context) {
135
153
  target: "stdout"
136
154
  }];
137
155
  }
138
- function evaluateGate(gate, nodeId, context, attempt) {
156
+ function evaluateGate(gate, nodeId, context, attempt, executor) {
139
157
  const gateId = gate.id ?? `${gate.kind}:${nodeId}`;
140
158
  const node = context.plan.graph.node(nodeId);
141
159
  switch (gate.kind) {
142
- case "command": return evaluateCommandGate(gate, gateId, nodeId, context);
160
+ case "command": return evaluateCommandGate(gate, gateId, nodeId, context, executor);
143
161
  case "artifact": return evaluateArtifactGate(gate, gateId, nodeId, context);
144
162
  case "builtin": return evaluateBuiltinGate(gate, gateId, nodeId, context);
145
163
  case "verdict": return evaluateVerdictGate(gate, gateId, nodeId, context, attempt);
@@ -152,8 +170,8 @@ function evaluateGate(gate, nodeId, context, attempt) {
152
170
  function assertNever(value) {
153
171
  throw new Error(`Unsupported gate kind: ${String(value)}`);
154
172
  }
155
- async function evaluateCommandGate(gate, gateId, nodeId, context) {
156
- const result = await executeCommand(gate.command ?? [], context, { timeout: gate.timeout_ms });
173
+ async function evaluateCommandGate(gate, gateId, nodeId, context, executor) {
174
+ const result = await Effect.runPromise(executor.execute(gate.command ?? [], context, { timeout: gate.timeout_ms }));
157
175
  const expected = gate.expect_exit_code ?? 0;
158
176
  return {
159
177
  evidence: result.evidence,
@@ -1,12 +1,12 @@
1
1
  import { parseJson } from "../../safe-json.js";
2
2
  import { parseHookResult } from "../../hooks.js";
3
3
  import { runtimeActorId } from "../actor-ids.js";
4
- import { executeCommand } from "../command-executor/command-executor.js";
5
- import "../command-executor/index.js";
6
4
  import { validateJsonSchemaSource } from "../json-validation/json-validation.js";
7
5
  import "../json-validation/index.js";
8
6
  import { emit, runtimeSystemId } from "../events/events.js";
9
7
  import "../events/index.js";
8
+ import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
9
+ import { Effect } from "effect";
10
10
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join, resolve } from "node:path";
12
12
  import { tmpdir } from "node:os";
@@ -225,34 +225,79 @@ function hookModuleSpecifier(hookFunction, context) {
225
225
  if (hookFunction.module.startsWith(".") || hookFunction.module.startsWith("/")) return pathToFileURL(resolve(context.worktreePath, hookFunction.module)).href;
226
226
  return hookFunction.module;
227
227
  }
228
- async function executeCommandHookFunction(hookFunction, binding, event, context, failure, node, gateId) {
229
- if (context.hookPolicy.allowCommandHooks === false) return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, ["command hooks are disabled"], node) };
230
- if (hookFunction.trusted !== true && (context.config.hooks.policy?.commands === "trusted-only" || context.hookPolicy.allowUntrustedCommandHooks === false)) return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, ["command hook is not trusted"], node) };
231
- if (context.config.hooks.policy?.commands === "deny") return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, ["command hooks are disabled"], node) };
232
- const tempDir = mkdtempSync(join(tmpdir(), "pipeline-hook-"));
233
- const inputPath = join(tempDir, "input.json");
234
- const resultPath = join(tempDir, "result.json");
235
- try {
236
- writeFileSync(inputPath, JSON.stringify(hookContext(context, event, binding, failure, node, gateId)));
237
- const commandResult = await executeCommand(hookFunction.command, context, {
238
- env: {
239
- ...hookEnv(hookFunction, context),
240
- PIPELINE_HOOK_INPUT: inputPath,
241
- PIPELINE_HOOK_RESULT: resultPath
242
- },
243
- extendEnv: false,
244
- outputLimitBytes: hookFunction.output_limit_bytes ?? context.hookPolicy.outputLimitBytes,
245
- timeout: hookFunction.timeout_ms ?? context.hookPolicy.timeoutMs
246
- });
228
+ function executeCommandHookFunction(hookFunction, binding, event, context, failure, node, gateId) {
229
+ const program = executeCommandHookFunctionEffect(hookFunction, binding, event, context, failure, node, gateId);
230
+ return Effect.runPromise(Effect.provide(program, CommandExecutorLive));
231
+ }
232
+ function executeCommandHookFunctionEffect(hookFunction, binding, event, context, failure, node, gateId) {
233
+ return Effect.gen(function* () {
234
+ const policyFailure = commandHookPolicyFailure(hookFunction, binding, context, node);
235
+ if (policyFailure) return { failure: policyFailure };
236
+ const files = yield* createHookTempFiles();
237
+ return yield* runCommandHookWithTempFiles(files, hookFunction, binding, event, context, failure, node, gateId).pipe(Effect.ensuring(removeHookTempDir(files.tempDir)));
238
+ });
239
+ }
240
+ function createHookTempFiles() {
241
+ return Effect.try(() => {
242
+ const tempDir = mkdtempSync(join(tmpdir(), "pipeline-hook-"));
243
+ return {
244
+ inputPath: join(tempDir, "input.json"),
245
+ resultPath: join(tempDir, "result.json"),
246
+ tempDir
247
+ };
248
+ });
249
+ }
250
+ function removeHookTempDir(tempDir) {
251
+ return Effect.sync(() => rmSync(tempDir, {
252
+ force: true,
253
+ recursive: true
254
+ }));
255
+ }
256
+ function commandHookPolicyFailure(hookFunction, binding, context, node) {
257
+ if (commandHooksDisabled(context)) return commandHookFailure(binding, "command hooks are disabled", node);
258
+ if (untrustedCommandHookDisabled(hookFunction, context)) return commandHookFailure(binding, "command hook is not trusted", node);
259
+ }
260
+ function commandHooksDisabled(context) {
261
+ return context.hookPolicy.allowCommandHooks === false || context.config.hooks.policy?.commands === "deny";
262
+ }
263
+ function untrustedCommandHookDisabled(hookFunction, context) {
264
+ const commandPolicy = context.config.hooks.policy?.commands;
265
+ return hookFunction.trusted !== true && (commandPolicy === "trusted-only" || context.hookPolicy.allowUntrustedCommandHooks === false);
266
+ }
267
+ function commandHookFailure(binding, evidence, node) {
268
+ return runtimeHookFailure(binding, `hook '${binding.id}' failed`, [evidence], node);
269
+ }
270
+ function runCommandHookWithTempFiles(files, hookFunction, binding, event, context, failure, node, gateId) {
271
+ return Effect.gen(function* () {
272
+ yield* writeCommandHookInput(files.inputPath, hookContext(context, event, binding, failure, node, gateId));
273
+ const commandResult = yield* (yield* CommandExecutor).execute(hookFunction.command, context, commandHookOptions(hookFunction, context, files));
247
274
  if (commandResult.exitCode !== 0) return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, commandResult.evidence, node) };
248
- if (!existsSync(resultPath)) return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, ["command hook did not write PIPELINE_HOOK_RESULT"], node) };
249
- return parseAndValidateHookResult(parseJson(readFileSync(resultPath, "utf8"), "hook result"), binding, hookFunction, context, node);
250
- } finally {
251
- rmSync(tempDir, {
252
- force: true,
253
- recursive: true
254
- });
255
- }
275
+ return yield* readCommandHookResult(files.resultPath, binding, hookFunction, context, node);
276
+ });
277
+ }
278
+ function writeCommandHookInput(inputPath, context) {
279
+ return Effect.try(() => writeFileSync(inputPath, JSON.stringify(context)));
280
+ }
281
+ function commandHookOptions(hookFunction, context, files) {
282
+ return {
283
+ env: {
284
+ ...hookEnv(hookFunction, context),
285
+ PIPELINE_HOOK_INPUT: files.inputPath,
286
+ PIPELINE_HOOK_RESULT: files.resultPath
287
+ },
288
+ extendEnv: false,
289
+ outputLimitBytes: hookFunction.output_limit_bytes ?? context.hookPolicy.outputLimitBytes,
290
+ timeout: hookFunction.timeout_ms ?? context.hookPolicy.timeoutMs
291
+ };
292
+ }
293
+ function readCommandHookResult(resultPath, binding, hookFunction, context, node) {
294
+ return Effect.gen(function* () {
295
+ if (!(yield* Effect.sync(() => existsSync(resultPath)))) return { failure: commandHookFailure(binding, "command hook did not write PIPELINE_HOOK_RESULT", node) };
296
+ return yield* parseCommandHookResult(resultPath, binding, hookFunction, context, node);
297
+ });
298
+ }
299
+ function parseCommandHookResult(resultPath, binding, hookFunction, context, node) {
300
+ return Effect.try(() => parseAndValidateHookResult(parseJson(readFileSync(resultPath, "utf8"), "hook result"), binding, hookFunction, context, node));
256
301
  }
257
302
  function hookContext(context, event, binding, failure, node, gateId) {
258
303
  const taskContext = node ? effectiveTaskContext(node, context) : context.taskContext;
@@ -1,5 +1,6 @@
1
- import { Data } from "effect";
2
- import { createOpencode, createOpencodeClient } from "@opencode-ai/sdk";
1
+ import { OpencodeSdkService, OpencodeSdkServiceLive } from "./services/opencode-sdk-service.js";
2
+ import { Data, Effect } from "effect";
3
+ import { createOpencodeClient } from "@opencode-ai/sdk";
3
4
  //#region src/runtime/opencode-server.ts
4
5
  const DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
5
6
  var OpencodeServerStartupError = class extends Data.TaggedError("OpencodeServerStartupError") {
@@ -10,30 +11,35 @@ var OpencodeServerStartupError = class extends Data.TaggedError("OpencodeServerS
10
11
  });
11
12
  }
12
13
  };
13
- async function openOpencodeServer(options) {
14
+ function openOpencodeServer(options) {
15
+ return Effect.runPromise(Effect.provide(openOpencodeServerEffect(options), OpencodeSdkServiceLive));
16
+ }
17
+ function openOpencodeServerEffect(options) {
14
18
  const externalUrl = options.serverUrl ?? process.env.OPENCODE_SERVER_URL ?? "";
15
- if (externalUrl.length > 0) return connectExternalServer(externalUrl, options.directory);
16
- return await spawnOwnedServer(options);
19
+ if (externalUrl.length > 0) return connectExternalServer(externalUrl, options.directory).pipe(Effect.mapError(startupError));
20
+ return spawnOwnedServer(options);
17
21
  }
18
22
  function connectExternalServer(url, directory) {
19
- return {
20
- close: () => Promise.resolve(),
21
- client: createOpencodeClient({
22
- baseUrl: url,
23
- directory
24
- }),
25
- owned: false,
26
- url
27
- };
23
+ return Effect.gen(function* () {
24
+ return {
25
+ close: () => Promise.resolve(),
26
+ client: yield* (yield* OpencodeSdkService).createClient({
27
+ baseUrl: url,
28
+ directory
29
+ }),
30
+ owned: false,
31
+ url
32
+ };
33
+ });
28
34
  }
29
- async function spawnOwnedServer(options) {
30
- const spawn = options.spawn ?? createOpencode;
31
- try {
32
- const { client, server } = await spawn(spawnArgs(options));
35
+ function spawnOwnedServer(options) {
36
+ return Effect.gen(function* () {
37
+ const { client, server } = yield* (yield* OpencodeSdkService).spawnServer(spawnArgs(options), options.spawn);
33
38
  return ownedHandle(client, server, options.directory);
34
- } catch (error) {
35
- throw new OpencodeServerStartupError(`Failed to start opencode server: ${errorText(error)}`, { cause: error });
36
- }
39
+ }).pipe(Effect.catchAll((error) => Effect.fail(startupError(error))));
40
+ }
41
+ function startupError(error) {
42
+ return new OpencodeServerStartupError(`Failed to start opencode server: ${errorText(error)}`, { cause: error });
37
43
  }
38
44
  function spawnArgs(options) {
39
45
  return {