@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.
- package/.agents/skills/orchestrate/SKILL.md +45 -32
- package/README.md +51 -41
- package/defaults/pipeline.yaml +13 -12
- package/defaults/profiles.yaml +1 -1
- package/dist/argo-submit.js +1 -1
- package/dist/cli/doctor.d.ts +21 -0
- package/dist/cli/doctor.js +268 -0
- package/dist/cli/format.js +6 -3
- package/dist/cli/program.d.ts +14 -16
- package/dist/cli/program.js +291 -104
- package/dist/cli/run-resolver.js +58 -0
- package/dist/commands/bench-command.js +12 -4
- package/dist/commands/pipeline-command.js +22 -5
- package/dist/commands/runner-command-command.js +32 -9
- package/dist/config/lint.js +44 -26
- package/dist/config/load.js +0 -1
- package/dist/config/schemas.d.ts +0 -6
- package/dist/config/schemas.js +0 -8
- package/dist/context/repo-map.js +72 -56
- package/dist/gates.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -14
- package/dist/install-commands/claude-code.js +4 -33
- package/dist/install-commands/opencode.js +119 -171
- package/dist/mcp/repo-local-backends.js +51 -39
- package/dist/moka-submit.js +3 -3
- package/dist/pipeline-runtime.js +15 -5
- package/dist/planning/generate.js +5 -11
- package/dist/run-control/commands.js +340 -0
- package/dist/run-control/contracts.d.ts +21 -0
- package/dist/run-control/contracts.js +129 -0
- package/dist/run-control/detach.js +79 -0
- package/dist/run-control/runtime-reporter.js +187 -0
- package/dist/run-control/store.js +304 -0
- package/dist/run-control/supervisor.js +192 -0
- package/dist/runner-command/finalize.js +28 -37
- package/dist/runner-command/lifecycle-context.js +130 -63
- package/dist/runner-command/lifecycle.js +22 -31
- package/dist/runner-command/run.js +120 -72
- package/dist/runner-command/task-descriptor.js +11 -4
- package/dist/runner.js +1 -1
- package/dist/runtime/agent-node/agent-node.js +3 -3
- package/dist/runtime/builtins/builtins.js +1 -3
- package/dist/runtime/changed-files/changed-files.js +1 -1
- package/dist/runtime/context/context.js +1 -1
- package/dist/runtime/contracts/contracts.d.ts +4 -0
- package/dist/runtime/hooks/hooks.js +1 -1
- package/dist/runtime/json-validation/json-validation.js +49 -23
- package/dist/runtime/local-scheduler.js +49 -26
- package/dist/runtime/opencode-adapter.js +14 -10
- package/dist/runtime/opencode-runtime.js +22 -20
- package/dist/runtime/opencode-session-executor.js +1 -1
- package/dist/runtime/parallel-node/parallel-node.js +10 -0
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +2 -35
- package/dist/runtime/run-journal.js +17 -10
- package/dist/runtime/services/file-system-service.js +29 -0
- package/dist/runtime/services/opencode-runtime-server-service.js +27 -0
- package/dist/runtime/services/repo-io-service.js +48 -0
- package/dist/runtime/services/run-journal-file-service.js +20 -0
- package/dist/runtime/services/runner-command-io-service.js +88 -0
- package/dist/runtime/workflow-lifecycle.js +76 -39
- package/dist/schedule/backlog-context.js +55 -29
- package/dist/schedule/passes/index.js +0 -1
- package/docs/config-architecture.md +4 -26
- package/docs/operator-guide.md +73 -32
- package/package.json +2 -1
- package/dist/runtime/select-candidate/select-candidate.js +0 -144
- package/dist/runtime/services/select-candidate-service.js +0 -13
- 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 {
|
|
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 {
|
|
9
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
34
|
-
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
|
-
|
|
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
|
|
50
|
-
const
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
yield* flushAndReport(sink, logger);
|
|
192
210
|
return result.status === "passed" ? EXIT_PASS : EXIT_FAIL;
|
|
193
|
-
}
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
239
|
-
if (task.kind === "prompt") return task.prompt;
|
|
240
|
-
if (task.path)
|
|
241
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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: "
|
|
256
|
-
}, "event.flush
|
|
257
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
14
|
-
return
|
|
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,
|
|
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 {
|
|
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
|
|
@@ -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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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 };
|