@oisincoveney/pipeline 2.7.0 → 2.8.1

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 (60) hide show
  1. package/defaults/pipeline.yaml +25 -0
  2. package/dist/argo-graph.js +7 -7
  3. package/dist/argo-submit.d.ts +2 -4
  4. package/dist/argo-submit.js +80 -80
  5. package/dist/bench/eval-report.js +27 -0
  6. package/dist/cli/program.js +19 -3
  7. package/dist/cluster-doctor.js +89 -101
  8. package/dist/commands/bench-command.js +18 -0
  9. package/dist/config/defaults.js +9 -19
  10. package/dist/config/load.js +47 -37
  11. package/dist/config/schemas.d.ts +24 -7
  12. package/dist/config/schemas.js +20 -7
  13. package/dist/context/repo-map.js +203 -0
  14. package/dist/install-commands/opencode.js +10 -1
  15. package/dist/mcp/gateway-error.js +15 -0
  16. package/dist/mcp/gateway.js +119 -220
  17. package/dist/moka-global-config.js +20 -20
  18. package/dist/moka-submit.d.ts +6 -6
  19. package/dist/pipeline-init.js +18 -12
  20. package/dist/pipeline-runtime.js +592 -372
  21. package/dist/planning/compile.d.ts +8 -3
  22. package/dist/planning/compile.js +7 -7
  23. package/dist/planning/generate.d.ts +6 -1
  24. package/dist/planning/generate.js +29 -7
  25. package/dist/run-state/git-refs.js +124 -94
  26. package/dist/runner-command-contract.d.ts +6 -1
  27. package/dist/runner-command-contract.js +6 -5
  28. package/dist/runner-event-schema.d.ts +6 -6
  29. package/dist/runner-event-sink.js +37 -68
  30. package/dist/runner.d.ts +6 -1
  31. package/dist/runner.js +3 -3
  32. package/dist/runtime/agent-node/agent-node.js +218 -159
  33. package/dist/runtime/changed-files/changed-files.js +15 -27
  34. package/dist/runtime/changed-files/index.js +2 -0
  35. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  36. package/dist/runtime/gates/gates.js +45 -27
  37. package/dist/runtime/hooks/hooks.js +74 -29
  38. package/dist/runtime/local-scheduler.js +45 -0
  39. package/dist/runtime/opencode-server.js +32 -23
  40. package/dist/runtime/opencode-session-executor.js +101 -44
  41. package/dist/runtime/parallel-node/parallel-node.js +93 -75
  42. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
  43. package/dist/runtime/run-journal.js +21 -0
  44. package/dist/runtime/scheduler.js +122 -93
  45. package/dist/runtime/select-candidate/select-candidate.js +52 -24
  46. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  47. package/dist/runtime/services/command-executor-service.js +8 -0
  48. package/dist/runtime/services/config-io-service.js +42 -0
  49. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  50. package/dist/runtime/services/git-porcelain-service.js +38 -0
  51. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  52. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  53. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  54. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  55. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  56. package/dist/runtime/services/select-candidate-service.js +13 -0
  57. package/dist/runtime/services/worktree-service.js +18 -0
  58. package/dist/schedule/passes/candidates.js +17 -8
  59. package/docs/config-architecture.md +105 -0
  60. package/package.json +7 -2
@@ -7,9 +7,14 @@ interface WorkflowPlannerIssue {
7
7
  message: string;
8
8
  path?: string;
9
9
  }
10
- declare class WorkflowPlannerError extends Error {
11
- code: WorkflowPlannerErrorCode;
12
- issues: WorkflowPlannerIssue[];
10
+ declare const WorkflowPlannerError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
11
+ readonly _tag: "WorkflowPlannerError";
12
+ } & Readonly<A>;
13
+ declare class WorkflowPlannerError extends WorkflowPlannerError_base<{
14
+ readonly code: WorkflowPlannerErrorCode;
15
+ readonly message: string;
16
+ readonly issues: WorkflowPlannerIssue[];
17
+ }> {
13
18
  constructor(code: WorkflowPlannerErrorCode, message: string, issues?: WorkflowPlannerIssue[]);
14
19
  }
15
20
  interface PlannedWorkflowNode {
@@ -1,15 +1,15 @@
1
1
  import { uniqueStrings } from "../strings.js";
2
2
  import { findDependencyCycles } from "./graph.js";
3
+ import { Data } from "effect";
3
4
  import { Graph } from "@dagrejs/graphlib";
4
5
  //#region src/planning/compile.ts
5
- var WorkflowPlannerError = class extends Error {
6
- code;
7
- issues;
6
+ var WorkflowPlannerError = class extends Data.TaggedError("WorkflowPlannerError") {
8
7
  constructor(code, message, issues = []) {
9
- super(message);
10
- this.name = "WorkflowPlannerError";
11
- this.code = code;
12
- this.issues = issues;
8
+ super({
9
+ code,
10
+ message,
11
+ issues
12
+ });
13
13
  }
14
14
  };
15
15
  function compileWorkflowPlan(config, workflowId = config.default_workflow) {
@@ -1594,7 +1594,12 @@ interface SchedulePlanningContext {
1594
1594
  workUnits: BacklogWorkUnit[];
1595
1595
  }
1596
1596
  declare function parseScheduleArtifact(source: string, sourcePath?: string): ScheduleArtifact;
1597
- declare class ScheduleArtifactError extends Error {
1597
+ declare const ScheduleArtifactError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
1598
+ readonly _tag: "ScheduleArtifactError";
1599
+ } & Readonly<A>;
1600
+ declare class ScheduleArtifactError extends ScheduleArtifactError_base<{
1601
+ readonly message: string;
1602
+ }> {
1598
1603
  constructor(message: string);
1599
1604
  }
1600
1605
  declare function compileScheduleArtifact(config: PipelineConfig, artifact: ScheduleArtifact, projectRoot?: string): CompiledScheduleArtifact;
@@ -17,6 +17,7 @@ import { plannerPrompt, plannerRepairPrompt } from "../schedule/prompts.js";
17
17
  import { compileWorkflowPlan } from "./compile.js";
18
18
  import { parseDocument, stringify } from "yaml";
19
19
  import { z } from "zod";
20
+ import { Data } from "effect";
20
21
  import { mkdirSync, writeFileSync } from "node:fs";
21
22
  import { join } from "node:path";
22
23
  //#region src/planning/generate.ts
@@ -30,10 +31,12 @@ const SCHEDULE_BUILTINS = [
30
31
  "duplication",
31
32
  "fallow",
32
33
  "lint",
34
+ "select-candidate",
33
35
  "semgrep",
34
36
  "test",
35
37
  "typecheck"
36
38
  ];
39
+ const PARALLEL_MERGE_BUILTINS = new Set(["drain-merge", "select-candidate"]);
37
40
  const scheduleArtifactSchema = z.object({
38
41
  generated_at: z.string().datetime(),
39
42
  kind: z.literal(SCHEDULE_KIND),
@@ -54,10 +57,9 @@ function parseScheduleArtifact(source, sourcePath = "schedule.yaml") {
54
57
  if (!parsed.success) throw new ScheduleArtifactError([`Invalid schedule artifact ${sourcePath}:`, ...parsed.error.issues.map((issue) => issue.path.length > 0 ? `- ${issue.path.join(".")}: ${issue.message}` : `- ${issue.message}`)].join("\n"));
55
58
  return parsed.data;
56
59
  }
57
- var ScheduleArtifactError = class extends Error {
60
+ var ScheduleArtifactError = class extends Data.TaggedError("ScheduleArtifactError") {
58
61
  constructor(message) {
59
- super(message);
60
- this.name = "ScheduleArtifactError";
62
+ super({ message });
61
63
  }
62
64
  };
63
65
  function compileScheduleArtifact(config, artifact, projectRoot) {
@@ -255,11 +257,28 @@ function unsafeParallelWorktreeIssues(config, artifact) {
255
257
  return [`parallel node '${workflowId}.${node.id}' has write-capable children sharing a worktree without isolated worktree roots or drain-merge integration`];
256
258
  }));
257
259
  }
260
+ function registerContainmentEdge(parent, child, index) {
261
+ const dependents = index.get(child.id) ?? [];
262
+ dependents.push(parent);
263
+ index.set(child.id, dependents);
264
+ }
265
+ function addParallelContainmentEdges(nodes, index) {
266
+ for (const node of nodes) {
267
+ if (node.kind !== "parallel") continue;
268
+ for (const child of node.nodes) registerContainmentEdge(node, child, index);
269
+ addParallelContainmentEdges(node.nodes, index);
270
+ }
271
+ }
272
+ function dependentsByNeedWithContainment(nested, flat) {
273
+ const index = dependentsByNeed(flat);
274
+ addParallelContainmentEdges(nested, index);
275
+ return index;
276
+ }
258
277
  function workflowNodeIssues(artifact, collectIssues) {
259
278
  return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
260
279
  const nodes = flattenWorkflowNodes(workflow.nodes);
261
280
  return collectIssues({
262
- dependentsByNeed: dependentsByNeed(nodes),
281
+ dependentsByNeed: dependentsByNeedWithContainment(workflow.nodes, nodes),
263
282
  nodes,
264
283
  workflowId
265
284
  });
@@ -272,7 +291,7 @@ function isWriteCapableParallelChild(config, node) {
272
291
  return false;
273
292
  }
274
293
  function hasDownstreamDrainMerge(nodeId, index) {
275
- return hasReachableDependent(nodeId, index, (node) => node.kind === "builtin" && node.builtin === "drain-merge");
294
+ return hasReachableDependent(nodeId, index, (node) => node.kind === "builtin" && PARALLEL_MERGE_BUILTINS.has(node.builtin));
276
295
  }
277
296
  function generatedRootWorkflowIssues(artifact) {
278
297
  const workflowIds = Object.keys(artifact.workflows);
@@ -324,9 +343,9 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
324
343
  const dependenciesByUnit = new Map(workUnits.map((unit) => [unit.id, (unit.dependencies ?? []).filter((id) => workUnitIds.has(id))]));
325
344
  return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
326
345
  const nodes = flattenWorkflowNodes(workflow.nodes);
327
- const index = dependentsByNeed(nodes);
346
+ const index = dependentsByNeedWithContainment(workflow.nodes, nodes);
328
347
  const nodesByWorkUnit = nodesByAssignedWorkUnit(nodes);
329
- return nodes.filter((node) => isImplementationNode(config, node)).flatMap((node) => {
348
+ return nodes.filter((node) => isImplementationNode(config, node) || isSelectCandidateNode(node)).flatMap((node) => {
330
349
  const dependentId = node.task_context?.id;
331
350
  if (!dependentId) return [];
332
351
  return (dependenciesByUnit.get(dependentId) ?? []).flatMap((prerequisiteId) => {
@@ -336,6 +355,9 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
336
355
  });
337
356
  });
338
357
  }
358
+ function isSelectCandidateNode(node) {
359
+ return node.kind === "builtin" && node.builtin === "select-candidate";
360
+ }
339
361
  function nodesByAssignedWorkUnit(nodes) {
340
362
  const grouped = /* @__PURE__ */ new Map();
341
363
  for (const node of nodes) {
@@ -1,14 +1,13 @@
1
+ import { GitPorcelainService, GitPorcelainServiceLive } from "../runtime/services/git-porcelain-service.js";
2
+ import { Effect } from "effect";
1
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
4
  import { dirname, resolve } from "node:path";
3
5
  import { tmpdir } from "node:os";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
6
  //#region src/run-state/git-refs.ts
7
7
  const DEFAULT_WORKSPACE_PATH = "/workspace";
8
8
  const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
9
9
  const WRITABLE_GIT_CREDENTIAL_STORE = resolve(tmpdir(), "pipeline-git-credentials");
10
10
  const SCP_LIKE_SSH_REMOTE_RE = /^[^@\s]+@[^:\s]+:.+/u;
11
- const execGit = promisify(execFile);
12
11
  let preparedBasicAuthCredentialStore;
13
12
  function runnerGitRefs(payload, nodeId) {
14
13
  const prefix = `refs/heads/pipeline/runs/${payload.run.id}/${payload.workflow.id}`;
@@ -19,101 +18,132 @@ function runnerGitRefs(payload, nodeId) {
19
18
  };
20
19
  }
21
20
  async function prepareRunnerGitWorkspace(payload, options = {}) {
22
- if (options.cwd) return resolve(options.cwd);
23
- const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
24
- mkdirSync(dirname(worktreePath), { recursive: true });
25
- await runGit(dirname(worktreePath), [
26
- "clone",
27
- "--no-tags",
28
- payload.repository.url,
29
- worktreePath
30
- ]);
31
- await runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
32
- return worktreePath;
21
+ return await Effect.runPromise(Effect.provide(prepareRunnerGitWorkspaceEffect(payload, options), GitPorcelainServiceLive));
22
+ }
23
+ function prepareRunnerGitWorkspaceEffect(payload, options) {
24
+ return Effect.gen(function* () {
25
+ if (options.cwd) return resolve(options.cwd);
26
+ const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
27
+ yield* Effect.sync(() => mkdirSync(dirname(worktreePath), { recursive: true }));
28
+ yield* runGit(dirname(worktreePath), [
29
+ "clone",
30
+ "--no-tags",
31
+ payload.repository.url,
32
+ worktreePath
33
+ ]);
34
+ yield* runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
35
+ return worktreePath;
36
+ });
33
37
  }
34
38
  async function mergeDependencyRefs(input) {
35
- await configureGitCommitter(input.worktreePath, input.committer);
36
- for (const nodeId of input.dependencyNodeIds) {
37
- const ref = runnerGitRefs(input.payload, nodeId).nodeRef;
38
- await runGit(input.worktreePath, [
39
+ return await Effect.runPromise(Effect.provide(mergeDependencyRefsEffect(input), GitPorcelainServiceLive));
40
+ }
41
+ function mergeDependencyRefsEffect(input) {
42
+ return Effect.gen(function* () {
43
+ yield* configureGitCommitter(input.worktreePath, input.committer);
44
+ yield* Effect.forEach(input.dependencyNodeIds, (nodeId) => mergeDependencyRef(input.worktreePath, runnerGitRefs(input.payload, nodeId).nodeRef));
45
+ });
46
+ }
47
+ function mergeDependencyRef(worktreePath, ref) {
48
+ return Effect.gen(function* () {
49
+ yield* runGit(worktreePath, [
39
50
  "fetch",
40
51
  "origin",
41
52
  ref
42
53
  ]);
43
- await runGit(input.worktreePath, [
54
+ yield* runGit(worktreePath, [
44
55
  "merge",
45
56
  "--no-ff",
46
57
  "--no-edit",
47
58
  "FETCH_HEAD"
48
59
  ]);
49
- }
60
+ });
50
61
  }
51
62
  async function commitAndPushNodeRef(input) {
52
- await commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
53
- const sha = (await runGit(input.worktreePath, ["rev-parse", "HEAD"])).trim();
54
- await runGit(input.worktreePath, [
55
- "push",
56
- "origin",
57
- `HEAD:${runnerGitRefs(input.payload, input.nodeId).nodeRef}`
58
- ]);
59
- return sha;
63
+ return await Effect.runPromise(Effect.provide(commitAndPushNodeRefEffect(input), GitPorcelainServiceLive));
64
+ }
65
+ function commitAndPushNodeRefEffect(input) {
66
+ return Effect.gen(function* () {
67
+ yield* commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
68
+ const sha = yield* headSha(input.worktreePath);
69
+ yield* runGit(input.worktreePath, [
70
+ "push",
71
+ "origin",
72
+ `HEAD:${runnerGitRefs(input.payload, input.nodeId).nodeRef}`
73
+ ]);
74
+ return sha;
75
+ });
60
76
  }
61
77
  async function promoteFinalRef(input) {
62
- await mergeDependencyRefs({
63
- committer: input.committer,
64
- dependencyNodeIds: input.sourceNodeIds,
65
- payload: input.payload,
66
- worktreePath: input.worktreePath
78
+ return await Effect.runPromise(Effect.provide(promoteFinalRefEffect(input), GitPorcelainServiceLive));
79
+ }
80
+ function promoteFinalRefEffect(input) {
81
+ return Effect.gen(function* () {
82
+ yield* mergeDependencyRefsEffect({
83
+ committer: input.committer,
84
+ dependencyNodeIds: input.sourceNodeIds,
85
+ payload: input.payload,
86
+ worktreePath: input.worktreePath
87
+ });
88
+ yield* commitChangesIfNeeded(input.worktreePath, "final", input.committer);
89
+ const sha = yield* headSha(input.worktreePath);
90
+ yield* runGit(input.worktreePath, [
91
+ "push",
92
+ "origin",
93
+ `HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
94
+ ]);
95
+ return sha;
96
+ });
97
+ }
98
+ function headSha(worktreePath) {
99
+ return Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
100
+ }
101
+ function commitChangesIfNeeded(worktreePath, nodeId, committer) {
102
+ return Effect.gen(function* () {
103
+ if ((yield* runGit(worktreePath, [
104
+ "status",
105
+ "--porcelain",
106
+ "--untracked-files=all"
107
+ ])).trim().length === 0) return;
108
+ yield* runGit(worktreePath, ["add", "--all"]);
109
+ yield* configureGitCommitter(worktreePath, committer);
110
+ yield* runGit(worktreePath, [
111
+ "commit",
112
+ "-m",
113
+ `pipeline: ${nodeId}`
114
+ ]);
115
+ });
116
+ }
117
+ function configureGitCommitter(worktreePath, committer) {
118
+ return Effect.gen(function* () {
119
+ yield* runGit(worktreePath, [
120
+ "config",
121
+ "--local",
122
+ "user.name",
123
+ committer.name
124
+ ]);
125
+ yield* runGit(worktreePath, [
126
+ "config",
127
+ "--local",
128
+ "user.email",
129
+ committer.email
130
+ ]);
67
131
  });
68
- await commitChangesIfNeeded(input.worktreePath, "final", input.committer);
69
- const sha = (await runGit(input.worktreePath, ["rev-parse", "HEAD"])).trim();
70
- await runGit(input.worktreePath, [
71
- "push",
72
- "origin",
73
- `HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
74
- ]);
75
- return sha;
76
- }
77
- async function commitChangesIfNeeded(worktreePath, nodeId, committer) {
78
- if ((await runGit(worktreePath, [
79
- "status",
80
- "--porcelain",
81
- "--untracked-files=all"
82
- ])).trim().length === 0) return;
83
- await runGit(worktreePath, ["add", "--all"]);
84
- await configureGitCommitter(worktreePath, committer);
85
- await runGit(worktreePath, [
86
- "commit",
87
- "-m",
88
- `pipeline: ${nodeId}`
89
- ]);
90
- }
91
- async function configureGitCommitter(worktreePath, committer) {
92
- await runGit(worktreePath, [
93
- "config",
94
- "--local",
95
- "user.name",
96
- committer.name
97
- ]);
98
- await runGit(worktreePath, [
99
- "config",
100
- "--local",
101
- "user.email",
102
- committer.email
103
- ]);
104
132
  }
105
133
  function runnerGitCommandArgs(args, remoteUrl) {
106
134
  return [...gitCredentialConfigArgs(remoteUrl), ...args];
107
135
  }
108
- async function runGit(cwd, args) {
109
- const remoteUrl = await remoteUrlFromGitArgs(cwd, args);
110
- assertSshCredentialsAvailable(remoteUrl);
111
- const { stdout } = await execGit("git", runnerGitCommandArgs(args, remoteUrl), {
112
- cwd,
113
- encoding: "utf8",
114
- env: runnerGitEnv(remoteUrl)
136
+ function runGit(cwd, args) {
137
+ return Effect.gen(function* () {
138
+ const remoteUrl = yield* remoteUrlFromGitArgs(cwd, args);
139
+ yield* Effect.try({
140
+ try: () => assertSshCredentialsAvailable(remoteUrl),
141
+ catch: (error) => error
142
+ });
143
+ const git = yield* GitPorcelainService;
144
+ const commandArgs = runnerGitCommandArgs(args, remoteUrl);
145
+ return yield* git.run(cwd, commandArgs, runnerGitEnv(remoteUrl));
115
146
  });
116
- return stdout;
117
147
  }
118
148
  function assertSshCredentialsAvailable(remoteUrl) {
119
149
  if (!(remoteUrl && isSshRemote(remoteUrl))) return;
@@ -227,12 +257,15 @@ function isOwnerReadOnly(path) {
227
257
  function readCredentialFile(path) {
228
258
  return readFileSync(path, "utf8").trim();
229
259
  }
230
- async function remoteUrlFromGitArgs(cwd, args) {
231
- const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
232
- if (literalRemoteUrl) return literalRemoteUrl;
233
- const remoteName = remoteNameFromGitArgs(args);
234
- if (!remoteName) return;
235
- return await gitRemoteUrl(cwd, remoteName);
260
+ function remoteUrlFromGitArgs(cwd, args) {
261
+ return Effect.gen(function* () {
262
+ const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
263
+ if (literalRemoteUrl) return literalRemoteUrl;
264
+ return yield* remoteUrlForName(cwd, remoteNameFromGitArgs(args));
265
+ });
266
+ }
267
+ function remoteUrlForName(cwd, remoteName) {
268
+ return remoteName ? gitRemoteUrl(cwd, remoteName) : Effect.succeed(void 0);
236
269
  }
237
270
  function literalRemoteUrlFromGitArgs(args) {
238
271
  if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
@@ -247,20 +280,17 @@ function remoteNameFromGitArgs(args) {
247
280
  if (remoteArg && !remoteArg.startsWith("-") && !isRemoteUrl(remoteArg)) return remoteArg;
248
281
  }
249
282
  }
250
- async function gitRemoteUrl(cwd, remoteName) {
251
- const { stdout } = await execGit("git", [
252
- "remote",
253
- "get-url",
254
- remoteName
255
- ], {
256
- cwd,
257
- encoding: "utf8",
258
- env: {
283
+ function gitRemoteUrl(cwd, remoteName) {
284
+ return Effect.gen(function* () {
285
+ return (yield* (yield* GitPorcelainService).run(cwd, [
286
+ "remote",
287
+ "get-url",
288
+ remoteName
289
+ ], {
259
290
  ...process.env,
260
291
  GIT_TERMINAL_PROMPT: "0"
261
- }
292
+ })).trim() || void 0;
262
293
  });
263
- return stdout.trim() || void 0;
264
294
  }
265
295
  function isRemoteUrl(value) {
266
296
  return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
@@ -140,8 +140,13 @@ interface RunnerCommandPayloadValidationIssue {
140
140
  message: string;
141
141
  path: string;
142
142
  }
143
- declare class RunnerCommandPayloadValidationError extends Error {
143
+ declare const RunnerCommandPayloadValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
144
+ readonly _tag: "RunnerCommandPayloadValidationError";
145
+ } & Readonly<A>;
146
+ declare class RunnerCommandPayloadValidationError extends RunnerCommandPayloadValidationError_base<{
147
+ readonly message: string;
144
148
  readonly issues: RunnerCommandPayloadValidationIssue[];
149
+ }> {
145
150
  constructor(message: string, issues: RunnerCommandPayloadValidationIssue[]);
146
151
  }
147
152
  interface BuildRunnerCommandPayloadOptions {
@@ -1,5 +1,6 @@
1
1
  import { parseJson } from "./safe-json.js";
2
2
  import { z } from "zod";
3
+ import { Data } from "effect";
3
4
  import { readFileSync } from "node:fs";
4
5
  import parseGitUrl from "git-url-parse";
5
6
  //#region src/runner-command-contract.ts
@@ -71,12 +72,12 @@ const runnerCommandPayloadSchema = z.object({
71
72
  task: runnerTaskSchema,
72
73
  workflow: runnerWorkflowIdentitySchema
73
74
  }).strict();
74
- var RunnerCommandPayloadValidationError = class extends Error {
75
- issues;
75
+ var RunnerCommandPayloadValidationError = class extends Data.TaggedError("RunnerCommandPayloadValidationError") {
76
76
  constructor(message, issues) {
77
- super(message);
78
- this.name = "RunnerCommandPayloadValidationError";
79
- this.issues = issues;
77
+ super({
78
+ message,
79
+ issues
80
+ });
80
81
  }
81
82
  };
82
83
  function resolveRunnerEventSinkAuthToken(options) {
@@ -10,8 +10,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
10
10
  at: z.ZodOptional<z.ZodString>;
11
11
  sequence: z.ZodNumber;
12
12
  type: z.ZodEnum<{
13
- "workflow.planned": "workflow.planned";
14
13
  "workflow.start": "workflow.start";
14
+ "workflow.planned": "workflow.planned";
15
15
  }>;
16
16
  workflowPlan: z.ZodObject<{
17
17
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -55,10 +55,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
55
55
  }>;
56
56
  }, z.core.$strip>;
57
57
  type: z.ZodEnum<{
58
+ "node.start": "node.start";
59
+ "node.finish": "node.finish";
58
60
  "agent.finish": "agent.finish";
59
61
  "agent.start": "agent.start";
60
- "node.finish": "node.finish";
61
- "node.start": "node.start";
62
62
  }>;
63
63
  }, z.core.$strip>, z.ZodObject<{
64
64
  at: z.ZodOptional<z.ZodString>;
@@ -180,8 +180,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
180
180
  at: z.ZodOptional<z.ZodString>;
181
181
  sequence: z.ZodNumber;
182
182
  type: z.ZodEnum<{
183
- "workflow.planned": "workflow.planned";
184
183
  "workflow.start": "workflow.start";
184
+ "workflow.planned": "workflow.planned";
185
185
  }>;
186
186
  workflowPlan: z.ZodObject<{
187
187
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -225,10 +225,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
225
225
  }>;
226
226
  }, z.core.$strip>;
227
227
  type: z.ZodEnum<{
228
+ "node.start": "node.start";
229
+ "node.finish": "node.finish";
228
230
  "agent.finish": "agent.finish";
229
231
  "agent.start": "agent.start";
230
- "node.finish": "node.finish";
231
- "node.start": "node.start";
232
232
  }>;
233
233
  }, z.core.$strip>, z.ZodObject<{
234
234
  at: z.ZodOptional<z.ZodString>;
@@ -1,38 +1,14 @@
1
1
  import { mapRuntimeEventToRunnerEventRecords } from "./runner-command-contract.js";
2
- import ky, { isHTTPError } from "ky";
2
+ import { RunnerEventSinkHttpService, RunnerEventSinkHttpServiceLive } from "./runtime/services/runner-event-sink-http-service.js";
3
+ import { Effect } from "effect";
3
4
  //#region src/runner-event-sink.ts
4
5
  const DEFAULT_BATCH_SIZE = 50;
5
6
  const DEFAULT_MAX_RETRIES = 0;
6
7
  const DEFAULT_RETRY_DELAY_MS = 250;
7
- const RETRYABLE_STATUS_CODES = [
8
- 408,
9
- 429,
10
- 500,
11
- 501,
12
- 502,
13
- 503,
14
- 504,
15
- 505,
16
- 506,
17
- 507,
18
- 508,
19
- 509,
20
- 510,
21
- 511
22
- ];
23
- var EventSinkHttpError = class extends Error {
24
- status;
25
- constructor(status, message) {
26
- super(message);
27
- this.name = "EventSinkHttpError";
28
- this.status = status;
29
- }
30
- };
31
8
  function createRunnerEventSink(options) {
32
- const batchSize = Math.max(1, options.batchSize ?? DEFAULT_BATCH_SIZE);
33
- const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
34
- if (!fetchImpl) throw new Error("Runner event sink requires fetch support");
35
- if (!options.authToken.trim()) throw new Error("Runner event sink requires an auth token");
9
+ const batchSize = positiveOrDefault(options.batchSize, DEFAULT_BATCH_SIZE);
10
+ const fetchImpl = resolveFetch(options.fetch);
11
+ assertAuthToken(options.authToken);
36
12
  const queue = [];
37
13
  let flushChain = Promise.resolve();
38
14
  let nextSequence = 1;
@@ -45,15 +21,17 @@ function createRunnerEventSink(options) {
45
21
  sequence
46
22
  };
47
23
  };
48
- const flushQueue = async () => {
49
- while (queue.length > 0) {
24
+ const flushQueueEffect = () => Effect.gen(function* () {
25
+ const service = yield* RunnerEventSinkHttpService;
26
+ while (hasQueuedEvents(queue)) {
50
27
  const batch = queue.slice(0, batchSize);
51
- await postBatch(options, fetchImpl, batch);
28
+ yield* service.postBatch(postBatchRequest(options, fetchImpl, batch));
52
29
  queue.splice(0, batch.length);
53
30
  }
54
- };
31
+ });
32
+ const runFlush = () => Effect.runPromise(Effect.provide(flushQueueEffect(), RunnerEventSinkHttpServiceLive));
55
33
  const runSerializedFlush = () => {
56
- const nextFlush = flushChain.then(flushQueue, flushQueue);
34
+ const nextFlush = flushChain.then(runFlush, runFlush);
57
35
  flushChain = nextFlush.catch(() => void 0);
58
36
  return nextFlush;
59
37
  };
@@ -144,41 +122,32 @@ function createRunnerEventSink(options) {
144
122
  }
145
123
  };
146
124
  }
147
- async function postBatch(options, fetchImpl, events) {
148
- const maxRetries = Math.max(0, options.maxRetries ?? DEFAULT_MAX_RETRIES);
149
- const retryDelayMs = Math.max(0, options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
150
- try {
151
- await ky.post(options.url, {
152
- fetch: kyFetchAdapter(fetchImpl),
153
- headers: { [options.authHeader ?? "Authorization"]: `Bearer ${options.authToken}` },
154
- json: { events },
155
- retry: {
156
- delay: () => retryDelayMs,
157
- limit: maxRetries,
158
- methods: ["post"],
159
- retryOnTimeout: true,
160
- statusCodes: RETRYABLE_STATUS_CODES
161
- }
162
- });
163
- } catch (err) {
164
- if (isHTTPError(err)) {
165
- let data = "";
166
- if (typeof err.data === "string") data = err.data;
167
- else if (err.data !== void 0) data = JSON.stringify(err.data);
168
- throw new EventSinkHttpError(err.response.status, `Event sink responded with ${err.response.status}${data ? `: ${data}` : ""}`);
169
- }
170
- throw err instanceof Error ? err : new Error(String(err));
171
- }
125
+ function hasQueuedEvents(queue) {
126
+ return queue.length > 0;
172
127
  }
173
- function kyFetchAdapter(fetchImpl) {
174
- return async (input, init) => {
175
- const request = new Request(input, init);
176
- return fetchImpl(request.url, {
177
- body: await request.clone().text(),
178
- headers: request.headers,
179
- method: request.method,
180
- signal: request.signal
181
- });
128
+ function positiveOrDefault(value, fallback) {
129
+ return Math.max(1, value ?? fallback);
130
+ }
131
+ function nonNegativeOrDefault(value, fallback) {
132
+ return Math.max(0, value ?? fallback);
133
+ }
134
+ function resolveFetch(fetchImpl) {
135
+ const resolved = fetchImpl ?? globalThis.fetch?.bind(globalThis);
136
+ if (!resolved) throw new Error("Runner event sink requires fetch support");
137
+ return resolved;
138
+ }
139
+ function assertAuthToken(authToken) {
140
+ if (!authToken.trim()) throw new Error("Runner event sink requires an auth token");
141
+ }
142
+ function postBatchRequest(options, fetchImpl, events) {
143
+ return {
144
+ authHeader: options.authHeader,
145
+ authToken: options.authToken,
146
+ events,
147
+ fetch: fetchImpl,
148
+ maxRetries: nonNegativeOrDefault(options.maxRetries, DEFAULT_MAX_RETRIES),
149
+ retryDelayMs: nonNegativeOrDefault(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS),
150
+ url: options.url
182
151
  };
183
152
  }
184
153
  function timestamp(now) {
package/dist/runner.d.ts CHANGED
@@ -80,7 +80,12 @@ interface RunnerLaunchInput {
80
80
  prompt: string;
81
81
  worktreePath: string;
82
82
  }
83
- declare class RunnerCapabilityError extends Error {
83
+ declare const RunnerCapabilityError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
84
+ readonly _tag: "RunnerCapabilityError";
85
+ } & Readonly<A>;
86
+ declare class RunnerCapabilityError extends RunnerCapabilityError_base<{
87
+ readonly message: string;
88
+ }> {
84
89
  constructor(message: string);
85
90
  }
86
91
  /**