@oisincoveney/pipeline 2.8.3 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.agents/skills/orchestrate/SKILL.md +45 -32
  2. package/README.md +51 -41
  3. package/defaults/pipeline.yaml +13 -12
  4. package/defaults/profiles.yaml +1 -1
  5. package/dist/argo-submit.js +1 -1
  6. package/dist/cli/doctor.d.ts +21 -0
  7. package/dist/cli/doctor.js +268 -0
  8. package/dist/cli/format.js +6 -3
  9. package/dist/cli/program.d.ts +14 -16
  10. package/dist/cli/program.js +291 -104
  11. package/dist/cli/run-resolver.js +58 -0
  12. package/dist/commands/bench-command.js +12 -4
  13. package/dist/commands/pipeline-command.js +22 -5
  14. package/dist/commands/runner-command-command.js +32 -9
  15. package/dist/config/lint.js +44 -26
  16. package/dist/config/load.js +0 -1
  17. package/dist/config/schemas.d.ts +0 -6
  18. package/dist/config/schemas.js +0 -8
  19. package/dist/context/repo-map.js +72 -56
  20. package/dist/gates.js +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +20 -14
  23. package/dist/install-commands/claude-code.js +4 -33
  24. package/dist/install-commands/opencode.js +119 -171
  25. package/dist/mcp/repo-local-backends.js +51 -39
  26. package/dist/moka-submit.js +3 -3
  27. package/dist/pipeline-runtime.js +15 -5
  28. package/dist/planning/generate.js +5 -11
  29. package/dist/run-control/commands.js +340 -0
  30. package/dist/run-control/contracts.d.ts +21 -0
  31. package/dist/run-control/contracts.js +129 -0
  32. package/dist/run-control/detach.js +79 -0
  33. package/dist/run-control/runtime-reporter.js +187 -0
  34. package/dist/run-control/store.js +304 -0
  35. package/dist/run-control/supervisor.js +192 -0
  36. package/dist/runner-command/finalize.js +28 -37
  37. package/dist/runner-command/lifecycle-context.js +130 -63
  38. package/dist/runner-command/lifecycle.js +22 -31
  39. package/dist/runner-command/run.js +120 -72
  40. package/dist/runner-command/task-descriptor.js +11 -4
  41. package/dist/runner.js +1 -1
  42. package/dist/runtime/agent-node/agent-node.js +3 -3
  43. package/dist/runtime/builtins/builtins.js +1 -3
  44. package/dist/runtime/changed-files/changed-files.js +1 -1
  45. package/dist/runtime/context/context.js +1 -1
  46. package/dist/runtime/contracts/contracts.d.ts +4 -0
  47. package/dist/runtime/hooks/hooks.js +1 -1
  48. package/dist/runtime/json-validation/json-validation.js +49 -23
  49. package/dist/runtime/local-scheduler.js +49 -26
  50. package/dist/runtime/opencode-adapter.js +14 -10
  51. package/dist/runtime/opencode-runtime.js +22 -20
  52. package/dist/runtime/opencode-session-executor.js +1 -1
  53. package/dist/runtime/parallel-node/parallel-node.js +10 -0
  54. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +2 -35
  55. package/dist/runtime/run-journal.js +17 -10
  56. package/dist/runtime/services/file-system-service.js +29 -0
  57. package/dist/runtime/services/opencode-runtime-server-service.js +27 -0
  58. package/dist/runtime/services/repo-io-service.js +48 -0
  59. package/dist/runtime/services/run-journal-file-service.js +20 -0
  60. package/dist/runtime/services/runner-command-io-service.js +88 -0
  61. package/dist/runtime/workflow-lifecycle.js +76 -39
  62. package/dist/schedule/backlog-context.js +55 -29
  63. package/dist/schedule/passes/index.js +0 -1
  64. package/docs/config-architecture.md +4 -26
  65. package/docs/operator-guide.md +73 -32
  66. package/package.json +2 -1
  67. package/dist/runtime/select-candidate/select-candidate.js +0 -144
  68. package/dist/runtime/services/select-candidate-service.js +0 -13
  69. package/dist/schedule/passes/candidates.js +0 -51
@@ -1,16 +1,14 @@
1
- import { readRunnerTaskDescriptor } from "./task-descriptor.js";
2
1
  import { loadPipelineConfig } from "../config/load.js";
3
2
  import "../config.js";
4
- import { compileScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
3
+ import { findPlannedNode } from "../planned-node.js";
5
4
  import { RunnerCommandPayloadValidationError, parseRunnerCommandPayload, resolveRunnerEventSinkAuthToken } from "../runner-command-contract.js";
6
- import { commitAndPushNodeRef, mergeDependencyRefs, prepareRunnerGitWorkspace } from "../run-state/git-refs.js";
7
5
  import { createRunnerEventSink } from "../runner-event-sink.js";
8
- import { findPlannedNode } from "../planned-node.js";
9
- import { runScheduledWorkflowTask } from "../pipeline-runtime.js";
6
+ import { RunnerCommandIoService, RunnerCommandIoServiceLive } from "../runtime/services/runner-command-io-service.js";
7
+ import { readRunnerTaskDescriptorEffect } from "./task-descriptor.js";
8
+ import { compileScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
10
9
  import { z } from "zod";
11
- import { readFileSync } from "node:fs";
10
+ import { Effect } from "effect";
12
11
  import { resolve } from "node:path";
13
- import { execa } from "execa";
14
12
  import pino from "pino";
15
13
  //#region src/runner-command/run.ts
16
14
  const runnerCommandOptionsSchema = z.object({
@@ -27,27 +25,47 @@ const EXIT_PASS = 0;
27
25
  const EXIT_FAIL = 1;
28
26
  const EXIT_VALIDATION = 64;
29
27
  const EXIT_STARTUP = 70;
30
- async function runRunnerCommand(rawOptions = {}) {
28
+ function runRunnerCommand(rawOptions = {}) {
31
29
  const parsedOptions = runnerCommandOptionsSchema.safeParse(rawOptions);
30
+ const stderr = isOutputStream(rawOptions.stderr) ? rawOptions.stderr : process.stderr;
31
+ const stdout = isOutputStream(rawOptions.stdout) ? rawOptions.stdout : process.stdout;
32
32
  const logger = createRunnerLogger({
33
- stderr: isOutputStream(rawOptions.stderr) ? rawOptions.stderr : process.stderr,
34
- stdout: isOutputStream(rawOptions.stdout) ? rawOptions.stdout : process.stdout
33
+ stderr,
34
+ stdout
35
35
  });
36
36
  if (!parsedOptions.success) {
37
37
  logger.error({
38
38
  error: parsedOptions.error.message,
39
39
  phase: "options.validate"
40
40
  }, "runner options validation failed");
41
- return EXIT_VALIDATION;
41
+ return Promise.resolve(EXIT_VALIDATION);
42
42
  }
43
43
  const options = parsedOptions.data;
44
- try {
44
+ return Effect.runPromise(Effect.provide(runRunnerCommandEffect(options, {
45
+ logger,
46
+ stderr,
47
+ stdout
48
+ }), RunnerCommandIoServiceLive));
49
+ }
50
+ function resolveRunnerTargetNode(payload, compiled, descriptor) {
51
+ return Effect.gen(function* () {
52
+ if (payload.workflow.id !== compiled.workflowId) return yield* Effect.fail(/* @__PURE__ */ new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`));
53
+ const node = findPlannedNode(compiled.plan.topologicalOrder, descriptor.nodeId);
54
+ if (!node) return yield* Effect.fail(/* @__PURE__ */ new Error(`Argo task '${descriptor.nodeId}' is not declared in workflow '${compiled.workflowId}'`));
55
+ return node;
56
+ });
57
+ }
58
+ function runRunnerCommandEffect(options, runtime) {
59
+ return Effect.gen(function* () {
60
+ const io = yield* RunnerCommandIoService;
61
+ const logger = runtime.logger;
45
62
  logger.info({
46
63
  phase: "payload.load",
47
64
  status: "start"
48
65
  }, "payload.load start");
49
- const payload = parseRunnerCommandPayload(readFileSync(options.payloadFile, "utf8"));
50
- const descriptor = readRunnerTaskDescriptor(options.taskDescriptorFile ?? "/etc/pipeline/task.json");
66
+ const payloadRaw = yield* io.readText(options.payloadFile);
67
+ const payload = yield* attemptSync(() => parseRunnerCommandPayload(payloadRaw));
68
+ const descriptor = yield* readRunnerTaskDescriptorEffect(options.taskDescriptorFile ?? "/etc/pipeline/task.json");
51
69
  logger.info({
52
70
  nodeId: descriptor.nodeId,
53
71
  phase: "payload.load",
@@ -76,7 +94,7 @@ async function runRunnerCommand(rawOptions = {}) {
76
94
  phase: "git.workspace.prepare",
77
95
  status: "start"
78
96
  }, "git.workspace.prepare start");
79
- const worktreePath = await prepareRunnerGitWorkspace(payload, { cwd: options.cwd });
97
+ const worktreePath = yield* io.prepareRunnerGitWorkspace(payload, { cwd: options.cwd });
80
98
  logger.info({
81
99
  phase: "git.workspace.prepare",
82
100
  status: "finish"
@@ -85,7 +103,7 @@ async function runRunnerCommand(rawOptions = {}) {
85
103
  phase: "config.load",
86
104
  status: "start"
87
105
  }, "config.load start");
88
- const baseConfig = loadPipelineConfig(worktreePath, { allowMissingLintFileReferences: true });
106
+ const baseConfig = yield* attemptSync(() => loadPipelineConfig(worktreePath, { allowMissingLintFileReferences: true }));
89
107
  logger.info({
90
108
  phase: "config.load",
91
109
  status: "finish"
@@ -94,22 +112,21 @@ async function runRunnerCommand(rawOptions = {}) {
94
112
  phase: "schedule.compile",
95
113
  status: "start"
96
114
  }, "schedule.compile start");
97
- const compiled = compileScheduleArtifact(baseConfig, parseScheduleArtifact(readFileSync(options.scheduleFile, "utf8"), options.scheduleFile), worktreePath);
115
+ const scheduleRaw = yield* io.readText(options.scheduleFile);
116
+ const compiled = yield* attemptSync(() => compileScheduleArtifact(baseConfig, parseScheduleArtifact(scheduleRaw, options.scheduleFile), worktreePath));
98
117
  logger.info({
99
118
  phase: "schedule.compile",
100
119
  status: "finish",
101
120
  workflowId: compiled.workflowId
102
121
  }, "schedule.compile finish");
103
- if (payload.workflow.id !== compiled.workflowId) throw new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`);
104
- const node = findPlannedNode(compiled.plan.topologicalOrder, descriptor.nodeId);
105
- if (!node) throw new Error(`Argo task '${descriptor.nodeId}' is not declared in workflow '${compiled.workflowId}'`);
122
+ const node = yield* resolveRunnerTargetNode(payload, compiled, descriptor);
106
123
  logger.info({
107
124
  dependencyCount: node.needs.length,
108
125
  nodeId: descriptor.nodeId,
109
126
  phase: "dependency.merge",
110
127
  status: "start"
111
128
  }, "dependency.merge start");
112
- await mergeDependencyRefs({
129
+ yield* io.mergeDependencyRefs({
113
130
  committer: compiled.config.runner_command.git.committer,
114
131
  dependencyNodeIds: node.needs,
115
132
  payload,
@@ -126,7 +143,7 @@ async function runRunnerCommand(rawOptions = {}) {
126
143
  phase: "setup.commands",
127
144
  status: "start"
128
145
  }, "setup.commands start");
129
- await runSetupCommands(baseConfig.runner_command.environment.setup, {
146
+ yield* runSetupCommands(baseConfig.runner_command.environment.setup, {
130
147
  env: options.env ?? process.env,
131
148
  logger,
132
149
  worktreePath
@@ -147,13 +164,14 @@ async function runRunnerCommand(rawOptions = {}) {
147
164
  phase: "task.run",
148
165
  status: "start"
149
166
  }, "task.run start");
150
- const result = await runScheduledWorkflowTask({
167
+ const taskText = yield* runnerTaskTextEffect(payload.task, worktreePath);
168
+ const result = yield* io.runScheduledWorkflowTask({
151
169
  config: compiled.config,
152
170
  hookPolicy: payload.hookPolicy,
153
171
  nodeId: descriptor.nodeId,
154
172
  reporter: (event) => sink.recordRuntimeEvent(event),
155
173
  runId: payload.run.id,
156
- task: runnerTaskText(payload.task, worktreePath),
174
+ task: taskText,
157
175
  workflowId: compiled.workflowId,
158
176
  worktreePath
159
177
  });
@@ -170,7 +188,7 @@ async function runRunnerCommand(rawOptions = {}) {
170
188
  phase: "git.node-ref.push",
171
189
  status: "start"
172
190
  }, "git.node-ref.push start");
173
- await commitAndPushNodeRef({
191
+ yield* io.commitAndPushNodeRef({
174
192
  committer: compiled.config.runner_command.git.committer,
175
193
  nodeId: descriptor.nodeId,
176
194
  payload,
@@ -188,40 +206,61 @@ async function runRunnerCommand(rawOptions = {}) {
188
206
  taskId: descriptor.nodeId,
189
207
  workflowId: payload.workflow.id
190
208
  });
191
- await flushAndReport(sink, logger);
209
+ yield* flushAndReport(sink, logger);
192
210
  return result.status === "passed" ? EXIT_PASS : EXIT_FAIL;
193
- } catch (error) {
194
- const message = error instanceof Error ? error.message : String(error);
195
- logger.error({
196
- error: message,
197
- phase: "runner-command"
198
- }, message);
199
- return error instanceof RunnerCommandPayloadValidationError || error instanceof z.ZodError ? EXIT_VALIDATION : EXIT_STARTUP;
200
- }
211
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => runnerCommandErrorExitCode(error, runtime.logger))));
201
212
  }
202
- async function runSetupCommands(commands, options) {
203
- for (const [index, command] of commands.entries()) {
204
- options.logger.info({
205
- command: command.command,
206
- index: index + 1,
207
- phase: "setup.command",
208
- status: "start"
209
- }, "setup.command start");
210
- const result = await execa(command.command, command.args, {
213
+ function attemptSync(try_) {
214
+ return Effect.try({
215
+ try: try_,
216
+ catch: (error) => error
217
+ });
218
+ }
219
+ function runnerCommandErrorExitCode(error, logger) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ logger.error({
222
+ error: message,
223
+ phase: "runner-command"
224
+ }, message);
225
+ return error instanceof RunnerCommandPayloadValidationError || error instanceof z.ZodError ? EXIT_VALIDATION : EXIT_STARTUP;
226
+ }
227
+ function runSetupCommands(commands, options) {
228
+ return Effect.forEach(commands.entries(), ([index, command]) => runSetupCommand(command, index, options)).pipe(Effect.asVoid);
229
+ }
230
+ function runSetupCommand(command, index, options) {
231
+ return Effect.gen(function* () {
232
+ const io = yield* RunnerCommandIoService;
233
+ const commandIndex = index + 1;
234
+ options.logger.info(setupCommandLog(command.command, commandIndex, "start"), "setup.command start");
235
+ const exitCode = setupExitCode((yield* io.runSetupCommand(command.command, command.args, {
211
236
  cwd: options.worktreePath,
212
- env: options.env,
213
- reject: false
214
- });
215
- options.logger.info({
216
- command: command.command,
217
- exitCode: result.exitCode,
218
- index: index + 1,
219
- phase: "setup.command",
220
- required: command.required,
221
- status: "finish"
222
- }, "setup.command finish");
223
- if (result.exitCode !== 0 && command.required) throw new Error(`runner setup command '${command.command}' failed with exit ${result.exitCode}`);
224
- }
237
+ env: options.env
238
+ })).exitCode);
239
+ options.logger.info(setupCommandFinishLog(command, commandIndex, exitCode), "setup.command finish");
240
+ if (exitCode !== 0 && command.required) return yield* Effect.fail(/* @__PURE__ */ new Error(`runner setup command '${command.command}' failed with exit ${exitCode}`));
241
+ });
242
+ }
243
+ function setupExitCode(exitCode) {
244
+ if (typeof exitCode === "number") return exitCode;
245
+ return 1;
246
+ }
247
+ function setupCommandLog(command, index, status) {
248
+ return {
249
+ command,
250
+ index,
251
+ phase: "setup.command",
252
+ status
253
+ };
254
+ }
255
+ function setupCommandFinishLog(command, index, exitCode) {
256
+ return {
257
+ command: command.command,
258
+ exitCode,
259
+ index,
260
+ phase: "setup.command",
261
+ required: command.required,
262
+ status: "finish"
263
+ };
225
264
  }
226
265
  function logFailedTaskRun(logger, nodeId, result) {
227
266
  if (result.status === "passed" && result.exitCode === 0) return;
@@ -235,32 +274,41 @@ function logFailedTaskRun(logger, nodeId, result) {
235
274
  status: "failed"
236
275
  }, "task.run failed");
237
276
  }
238
- function runnerTaskText(task, worktreePath) {
239
- if (task.kind === "prompt") return task.prompt;
240
- if (task.path) return readFileSync(resolve(worktreePath, task.path), "utf8");
241
- return [task.id, task.title].filter(Boolean).join(" ");
277
+ function runnerTaskTextEffect(task, worktreePath) {
278
+ if (task.kind === "prompt") return Effect.succeed(task.prompt);
279
+ if (task.path) {
280
+ const taskPath = task.path;
281
+ return Effect.gen(function* () {
282
+ return yield* (yield* RunnerCommandIoService).readText(resolve(worktreePath, taskPath));
283
+ });
284
+ }
285
+ return Effect.succeed([task.id, task.title].filter(Boolean).join(" "));
242
286
  }
243
287
  function isOutputStream(value) {
244
288
  return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
245
289
  }
246
- async function flushAndReport(sink, logger) {
247
- logger.info({
248
- phase: "event.flush",
249
- status: "start"
250
- }, "event.flush start");
251
- try {
252
- await sink.flush();
290
+ function flushAndReport(sink, logger) {
291
+ return Effect.gen(function* () {
292
+ const io = yield* RunnerCommandIoService;
253
293
  logger.info({
254
294
  phase: "event.flush",
255
- status: "finish"
256
- }, "event.flush finish");
257
- } catch (error) {
295
+ status: "start"
296
+ }, "event.flush start");
297
+ const result = yield* Effect.either(io.flushSink(sink));
298
+ if (result._tag === "Right") {
299
+ logger.info({
300
+ phase: "event.flush",
301
+ status: "finish"
302
+ }, "event.flush finish");
303
+ return;
304
+ }
305
+ const error = result.left;
258
306
  const message = error instanceof Error ? error.message : String(error);
259
307
  logger.error({
260
308
  error: message,
261
309
  phase: "event.flush"
262
310
  }, `runner event flush failed: ${message}`);
263
- }
311
+ });
264
312
  }
265
313
  function createRunnerLogger(options) {
266
314
  const streams = [{
@@ -291,4 +339,4 @@ function createRunnerLogger(options) {
291
339
  }, pino.multistream(streams, { dedupe: true }));
292
340
  }
293
341
  //#endregion
294
- export { runRunnerCommand, runnerTaskText };
342
+ export { runRunnerCommand, runnerTaskTextEffect };
@@ -1,6 +1,7 @@
1
1
  import { parseJson } from "../safe-json.js";
2
+ import { RunnerCommandIoService } from "../runtime/services/runner-command-io-service.js";
2
3
  import { z } from "zod";
3
- import { readFileSync } from "node:fs";
4
+ import { Effect } from "effect";
4
5
  //#region src/runner-command/task-descriptor.ts
5
6
  const DEFAULT_RUNNER_TASK_DESCRIPTOR_PATH = "/etc/pipeline/task.json";
6
7
  const runnerTaskDescriptorSchema = z.object({ nodeId: z.string().min(1) }).strict();
@@ -10,8 +11,14 @@ function buildRunnerTaskDescriptor(nodeId) {
10
11
  function parseRunnerTaskDescriptor(raw) {
11
12
  return runnerTaskDescriptorSchema.parse(parseJson(raw, "runner task descriptor JSON"));
12
13
  }
13
- function readRunnerTaskDescriptor(path = DEFAULT_RUNNER_TASK_DESCRIPTOR_PATH) {
14
- return parseRunnerTaskDescriptor(readFileSync(path, "utf8"));
14
+ function readRunnerTaskDescriptorEffect(path = DEFAULT_RUNNER_TASK_DESCRIPTOR_PATH) {
15
+ return Effect.gen(function* () {
16
+ const raw = yield* (yield* RunnerCommandIoService).readText(path);
17
+ return yield* Effect.try({
18
+ try: () => parseRunnerTaskDescriptor(raw),
19
+ catch: (error) => error
20
+ });
21
+ });
15
22
  }
16
23
  //#endregion
17
- export { DEFAULT_RUNNER_TASK_DESCRIPTOR_PATH, buildRunnerTaskDescriptor, readRunnerTaskDescriptor };
24
+ export { DEFAULT_RUNNER_TASK_DESCRIPTOR_PATH, buildRunnerTaskDescriptor, readRunnerTaskDescriptorEffect };
package/dist/runner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Data } from "effect";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
3
  import { execa } from "execa";
4
+ import { join } from "node:path";
5
5
  //#region src/runner.ts
6
6
  var RunnerCapabilityError = class extends Data.TaggedError("RunnerCapabilityError") {
7
7
  constructor(message) {
@@ -1,14 +1,14 @@
1
1
  import { resolvePackageAssetPath } from "../../package-assets.js";
2
2
  import { resolveFileReference } from "../../path-refs.js";
3
+ import { gatewayServerForProfile } from "../../mcp/gateway.js";
4
+ import { selectNodeModel } from "../../model-resolver.js";
3
5
  import { createRunnerLaunchPlan } from "../../runner.js";
4
6
  import { normalizeRunnerOutput, runnerTextCandidates } from "../../runner-output.js";
7
+ import { estimateTokens } from "../../token-estimator.js";
5
8
  import { normalizeJsonSource, readJsonSchemaSource, validateJsonSchemaSource } from "../json-validation/json-validation.js";
6
9
  import "../json-validation/index.js";
7
10
  import { emit, emitAgentFinish, emitAgentStart } from "../events/events.js";
8
11
  import "../events/index.js";
9
- import { gatewayServerForProfile } from "../../mcp/gateway.js";
10
- import { selectNodeModel } from "../../model-resolver.js";
11
- import { estimateTokens } from "../../token-estimator.js";
12
12
  import { handoffFinalizerPrompt, parseHandoff, renderHandoff, synthesizeMinimalHandoff } from "../handoff.js";
13
13
  import { AgentNodeRuntimeService, AgentNodeRuntimeServiceLive } from "../services/agent-node-runtime-service.js";
14
14
  import { Effect } from "effect";
@@ -1,8 +1,7 @@
1
1
  import { parseJson } from "../../safe-json.js";
2
- import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
3
2
  import { executeDrainMergeBuiltin } from "../drain-merge/drain-merge.js";
4
3
  import "../drain-merge/index.js";
5
- import { executeSelectCandidateBuiltin } from "../select-candidate/select-candidate.js";
4
+ import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
6
5
  import { Effect } from "effect";
7
6
  import { existsSync, readFileSync, renameSync } from "node:fs";
8
7
  import { join } from "node:path";
@@ -29,7 +28,6 @@ const BUILTIN_HANDLERS = {
29
28
  duplication: (context) => executeDuplicationBuiltinEffect(context),
30
29
  fallow: (context) => executeFallowBuiltinEffect(context),
31
30
  lint: (context) => executeScriptBuiltinEffect(context, "lint"),
32
- "select-candidate": (context, node) => Effect.tryPromise(() => executeSelectCandidateBuiltin(context, node)),
33
31
  semgrep: (context) => executeSemgrepBuiltinEffect(context),
34
32
  test: (context) => executeTestBuiltinEffect(context),
35
33
  typecheck: (context) => executeScriptBuiltinEffect(context, "typecheck")
@@ -1,8 +1,8 @@
1
1
  import { GitPorcelainService, GitPorcelainServiceLive } from "../services/git-porcelain-service.js";
2
2
  import { Effect } from "effect";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { createHash } from "node:crypto";
5
4
  import { join } from "node:path";
5
+ import { createHash } from "node:crypto";
6
6
  //#region src/runtime/changed-files/changed-files.ts
7
7
  function snapshotChangedFiles(worktreePath) {
8
8
  return Effect.runSync(Effect.provide(snapshotChangedFilesEffect(worktreePath), GitPorcelainServiceLive));
@@ -1,9 +1,9 @@
1
1
  import { loadPipelineConfig } from "../../config/load.js";
2
2
  import "../../config.js";
3
3
  import { runLaunchPlan } from "../../runner.js";
4
- import { compileWorkflowPlan } from "../../planning/compile.js";
5
4
  import { createPublicRuntimeObservabilityEmitter } from "../events/events.js";
6
5
  import "../events/index.js";
6
+ import { compileWorkflowPlan } from "../../planning/compile.js";
7
7
  import { initialNodeStateStore } from "../node-state-store.js";
8
8
  import { randomUUID } from "node:crypto";
9
9
  //#region src/runtime/context/context.ts
@@ -146,6 +146,10 @@ type PipelineRuntimeEvent = {
146
146
  profile?: string;
147
147
  schemaPath?: string;
148
148
  type: "node.output.recorded";
149
+ } | {
150
+ nodeId: string;
151
+ sessionId: string;
152
+ type: "node.session";
149
153
  } | {
150
154
  attempt: number;
151
155
  nodeId: string;
@@ -1,11 +1,11 @@
1
1
  import { parseJson } from "../../safe-json.js";
2
- import { parseHookResult } from "../../hooks.js";
3
2
  import { runtimeActorId } from "../actor-ids.js";
4
3
  import { validateJsonSchemaSource } from "../json-validation/json-validation.js";
5
4
  import "../json-validation/index.js";
6
5
  import { emit, runtimeSystemId } from "../events/events.js";
7
6
  import "../events/index.js";
8
7
  import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
8
+ import { parseHookResult } from "../../hooks.js";
9
9
  import { Effect } from "effect";
10
10
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join, resolve } from "node:path";
@@ -1,6 +1,7 @@
1
- import { isRecord, parseJson, parseJsonRecord } from "../../safe-json.js";
2
1
  import { standardOutputSchemaJson, standardOutputSchemaNameFromPath } from "../../standard-output-schemas.js";
3
- import { existsSync, readFileSync } from "node:fs";
2
+ import { isRecord, parseJson, parseJsonRecord } from "../../safe-json.js";
3
+ import { FileSystemService, FileSystemServiceLive, runFileSystemSync } from "../services/file-system-service.js";
4
+ import { Effect } from "effect";
4
5
  import { join } from "node:path";
5
6
  import Ajv from "ajv";
6
7
  import addFormats from "ajv-formats";
@@ -25,32 +26,45 @@ function parseRuntimeOutput(format, output) {
25
26
  }
26
27
  }
27
28
  function validateJsonSchemaSource(source, schemaPath, worktreePath) {
28
- try {
29
- const schemaSource = readJsonSchemaSource(schemaPath, worktreePath);
30
- const value = parseJson(normalizeJsonSource(source), "JSON schema gate value");
31
- const validate = compiledJsonSchemaValidator(schemaPath, schemaSource);
32
- const errors = validate(value) ? [] : formatJsonSchemaErrors(validate.errors ?? []);
33
- return {
34
- evidence: errors.length === 0 ? [`JSON schema passed: ${schemaPath}`] : errors.map((error) => `schema: ${error}`),
35
- passed: errors.length === 0,
36
- reason: errors.length === 0 ? void 0 : "JSON schema validation failed"
37
- };
38
- } catch (err) {
39
- return {
40
- evidence: [err instanceof Error ? err.message : String(err)],
41
- passed: false,
42
- reason: "JSON schema validation failed"
43
- };
44
- }
29
+ return runFileSystemSync(validateJsonSchemaSourceEffect(source, schemaPath, worktreePath), FileSystemServiceLive);
30
+ }
31
+ function validateJsonSchemaSourceEffect(source, schemaPath, worktreePath) {
32
+ return Effect.catchAll(Effect.gen(function* () {
33
+ const schemaSource = yield* readJsonSchemaSourceEffect(schemaPath, worktreePath);
34
+ return yield* Effect.try({
35
+ try: () => validateJsonSchemaValue(source, schemaPath, schemaSource),
36
+ catch: (error) => error
37
+ });
38
+ }), (error) => Effect.succeed(jsonSchemaValidationFailure(error)));
39
+ }
40
+ function validateJsonSchemaValue(source, schemaPath, schemaSource) {
41
+ const value = parseJson(normalizeJsonSource(source), "JSON schema gate value");
42
+ const validate = compiledJsonSchemaValidator(schemaPath, schemaSource);
43
+ return jsonSchemaValidationSuccess(schemaPath, validate(value) ? [] : formatJsonSchemaErrors(validate.errors ?? []));
44
+ }
45
+ function jsonSchemaValidationSuccess(schemaPath, errors) {
46
+ return {
47
+ evidence: jsonSchemaValidationEvidence(schemaPath, errors),
48
+ passed: errors.length === 0,
49
+ reason: errors.length === 0 ? void 0 : "JSON schema validation failed"
50
+ };
51
+ }
52
+ function jsonSchemaValidationEvidence(schemaPath, errors) {
53
+ return errors.length === 0 ? [`JSON schema passed: ${schemaPath}`] : errors.map((error) => `schema: ${error}`);
45
54
  }
46
55
  function normalizeJsonSource(source) {
47
56
  const trimmed = source.trim();
48
57
  return MARKDOWN_JSON_FENCE_RE.exec(trimmed)?.[1].trim() ?? trimmed;
49
58
  }
50
59
  function readJsonSchemaSource(schemaPath, worktreePath) {
60
+ return runFileSystemSync(readJsonSchemaSourceEffect(schemaPath, worktreePath), FileSystemServiceLive);
61
+ }
62
+ function readJsonSchemaSourceEffect(schemaPath, worktreePath) {
51
63
  const standardName = standardOutputSchemaNameFromPath(schemaPath);
52
- if (standardName) return standardOutputSchemaJson(standardName);
53
- return readFileSync(join(worktreePath, schemaPath), "utf8");
64
+ if (standardName) return Effect.succeed(standardOutputSchemaJson(standardName));
65
+ return Effect.gen(function* () {
66
+ return yield* (yield* FileSystemService).readText(join(worktreePath, schemaPath));
67
+ });
54
68
  }
55
69
  function compiledJsonSchemaValidator(schemaPath, schemaSource) {
56
70
  const cached = jsonSchemaValidatorCache.get(schemaPath);
@@ -68,8 +82,20 @@ function isJsonSchema(value) {
68
82
  return typeof value === "boolean" || isRecord(value);
69
83
  }
70
84
  function readOptionalFile(path) {
71
- if (!existsSync(path)) return null;
72
- return readFileSync(path, "utf8");
85
+ return runFileSystemSync(readOptionalFileEffect(path), FileSystemServiceLive);
86
+ }
87
+ function readOptionalFileEffect(path) {
88
+ return Effect.gen(function* () {
89
+ const fileSystem = yield* FileSystemService;
90
+ return (yield* fileSystem.exists(path)) ? yield* fileSystem.readText(path) : null;
91
+ });
92
+ }
93
+ function jsonSchemaValidationFailure(error) {
94
+ return {
95
+ evidence: [error instanceof Error ? error.message : String(error)],
96
+ passed: false,
97
+ reason: "JSON schema validation failed"
98
+ };
73
99
  }
74
100
  function formatJsonSchemaErrors(errors) {
75
101
  return errors.map((error) => {
@@ -1,6 +1,11 @@
1
- import { runWorkflowLifecycle } from "./workflow-lifecycle.js";
2
1
  import { runWorkflowScheduler } from "./scheduler.js";
2
+ import { runWorkflowLifecycle } from "./workflow-lifecycle.js";
3
+ import { Context, Effect, Layer } from "effect";
3
4
  //#region src/runtime/local-scheduler.ts
5
+ var LocalSchedulerOptionsService = class extends Context.Tag("LocalSchedulerOptionsService")() {};
6
+ function localSchedulerOptionsLive(options) {
7
+ return Layer.succeed(LocalSchedulerOptionsService, options);
8
+ }
4
9
  /**
5
10
  * The local, in-process scheduler seam. It owns workflow lifecycle hooks and
6
11
  * delegates the topological fan-out/gating loop to `runWorkflowScheduler`,
@@ -14,32 +19,50 @@ var LocalScheduler = class {
14
19
  async runWorkflow(plan, context) {
15
20
  const options = this.options;
16
21
  if (!options) throw new Error("LocalScheduler requires runtime options to run workflow");
17
- return (await runWorkflowLifecycle({
18
- buildResult: options.buildResult,
19
- emitWorkflowPlanned: () => options.emitWorkflowPlanned(context),
20
- emitWorkflowStarted: () => options.emitWorkflowStarted(context),
21
- executeWorkflow: () => runWorkflowScheduler({
22
- failFast: plan.execution.failFast,
23
- fanOutWidth: context.config.token_budget?.fan_out_width,
24
- isCancelled: () => options.isCancelled(context),
25
- journal: options.resolveJournal?.(context),
26
- markNodeReady: (nodeId) => options.markNodeReady(nodeId, context),
27
- maxParallelNodes: context.maxParallelNodes,
28
- nodes: plan.topologicalOrder.map((node) => ({
29
- category: node.category,
30
- dependents: node.dependents,
31
- id: node.id,
32
- index: node.index,
33
- needs: node.needs
34
- })),
35
- runNode: (nodeId) => options.executeNode(nodeId, context),
36
- shouldContinueAfterNodeResult: (result) => options.shouldContinueAfterNodeResult(result, context),
37
- skipNode: (nodeId, reason) => options.skipNode(nodeId, reason, context)
38
- }),
39
- isCancelled: () => options.isCancelled(context),
40
- runWorkflowHook: (event, failure) => options.runWorkflowHook(event, failure, context)
41
- })).result;
22
+ return await Effect.runPromise(Effect.provide(runLocalWorkflowEffect(plan, context), localSchedulerOptionsLive(options)));
42
23
  }
43
24
  };
25
+ function runLocalWorkflowEffect(plan, context) {
26
+ return Effect.gen(function* () {
27
+ const options = yield* LocalSchedulerOptionsService;
28
+ return (yield* Effect.tryPromise({
29
+ catch: (error) => error,
30
+ try: () => runWorkflowLifecycle(lifecycleInput(plan, context, options))
31
+ })).result;
32
+ });
33
+ }
34
+ function lifecycleInput(plan, context, options) {
35
+ return {
36
+ buildResult: options.buildResult,
37
+ emitWorkflowPlanned: () => options.emitWorkflowPlanned(context),
38
+ emitWorkflowStarted: () => options.emitWorkflowStarted(context),
39
+ executeWorkflow: () => runWorkflowScheduler(schedulerInput(plan, context, options)),
40
+ isCancelled: () => options.isCancelled(context),
41
+ runWorkflowHook: (event, failure) => options.runWorkflowHook(event, failure, context)
42
+ };
43
+ }
44
+ function schedulerInput(plan, context, options) {
45
+ return {
46
+ failFast: plan.execution.failFast,
47
+ fanOutWidth: context.config.token_budget?.fan_out_width,
48
+ isCancelled: () => options.isCancelled(context),
49
+ journal: options.resolveJournal?.(context),
50
+ markNodeReady: (nodeId) => options.markNodeReady(nodeId, context),
51
+ maxParallelNodes: context.maxParallelNodes,
52
+ nodes: plan.topologicalOrder.map(scheduleNode),
53
+ runNode: (nodeId) => options.executeNode(nodeId, context),
54
+ shouldContinueAfterNodeResult: (result) => options.shouldContinueAfterNodeResult(result, context),
55
+ skipNode: (nodeId, reason) => options.skipNode(nodeId, reason, context)
56
+ };
57
+ }
58
+ function scheduleNode(node) {
59
+ return {
60
+ category: node.category,
61
+ dependents: node.dependents,
62
+ id: node.id,
63
+ index: node.index,
64
+ needs: node.needs
65
+ };
66
+ }
44
67
  //#endregion
45
68
  export { LocalScheduler };