@oisincoveney/pipeline 2.7.0 → 2.8.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/defaults/pipeline.yaml +25 -0
- package/dist/argo-graph.js +7 -7
- package/dist/bench/eval-report.js +27 -0
- package/dist/cli/program.js +19 -3
- package/dist/commands/bench-command.js +18 -0
- package/dist/config/load.js +17 -0
- package/dist/config/schemas.d.ts +21 -4
- package/dist/config/schemas.js +20 -7
- package/dist/context/repo-map.js +203 -0
- package/dist/install-commands/opencode.js +10 -1
- package/dist/mcp/gateway.js +3 -3
- package/dist/moka-submit.d.ts +1 -1
- package/dist/pipeline-init.js +18 -12
- package/dist/pipeline-runtime.js +12 -1
- package/dist/planning/compile.d.ts +8 -3
- package/dist/planning/compile.js +7 -7
- package/dist/planning/generate.d.ts +6 -1
- package/dist/planning/generate.js +29 -7
- package/dist/runner-command-contract.d.ts +8 -3
- package/dist/runner-command-contract.js +6 -5
- package/dist/runner-event-sink.js +6 -5
- package/dist/runner.d.ts +6 -1
- package/dist/runner.js +3 -3
- package/dist/runtime/agent-node/agent-node.js +22 -4
- package/dist/runtime/local-scheduler.js +45 -0
- package/dist/runtime/opencode-server.js +6 -3
- package/dist/runtime/parallel-node/parallel-node.js +74 -75
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
- package/dist/runtime/run-journal.js +21 -0
- package/dist/runtime/scheduler.js +122 -93
- package/dist/runtime/select-candidate/select-candidate.js +13 -1
- package/dist/runtime/services/worktree-service.js +18 -0
- package/dist/schedule/passes/candidates.js +17 -8
- package/docs/config-architecture.md +105 -0
- package/package.json +7 -2
package/dist/pipeline-runtime.js
CHANGED
|
@@ -18,12 +18,14 @@ import "./runtime/builtins/index.js";
|
|
|
18
18
|
import { diffChangedFiles, snapshotChangedFiles } from "./runtime/changed-files/changed-files.js";
|
|
19
19
|
import { evaluateNodeGates } from "./runtime/gates/gates.js";
|
|
20
20
|
import "./runtime/gates/index.js";
|
|
21
|
+
import { LocalScheduler } from "./runtime/local-scheduler.js";
|
|
21
22
|
import { NodeStateTracker } from "./runtime/node-state-tracker.js";
|
|
22
23
|
import { configUsesOpencode, leaseOpencodeRuntime } from "./runtime/opencode-runtime.js";
|
|
23
24
|
import { executeParallelNode } from "./runtime/parallel-node/parallel-node.js";
|
|
24
25
|
import "./runtime/parallel-node/index.js";
|
|
25
26
|
import { decideNodeRetry, nodeRetryPolicy } from "./runtime/retry.js";
|
|
26
|
-
import {
|
|
27
|
+
import { fileRunJournal } from "./runtime/run-journal.js";
|
|
28
|
+
import { join } from "node:path";
|
|
27
29
|
//#region src/pipeline-runtime.ts
|
|
28
30
|
function runPipelineFromConfig(options) {
|
|
29
31
|
return withOpencodeRuntime(options, (resolved) => runPipelineWithContext(createRuntimeContext(resolved)));
|
|
@@ -78,6 +80,14 @@ async function runWithLeasedOpencode(options, config, worktreePath, run) {
|
|
|
78
80
|
await lease.release();
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
function runJournalPath(context, dir) {
|
|
84
|
+
return join(context.worktreePath ?? process.cwd(), dir, `${context.runId}.jsonl`);
|
|
85
|
+
}
|
|
86
|
+
function resolveRunJournal(context) {
|
|
87
|
+
const durability = context.config.durability;
|
|
88
|
+
if (!(durability?.enabled && context.runId)) return;
|
|
89
|
+
return fileRunJournal(runJournalPath(context, durability.dir));
|
|
90
|
+
}
|
|
81
91
|
async function runPipelineWithContext(context) {
|
|
82
92
|
return finishRuntime(context, await new LocalScheduler({
|
|
83
93
|
buildResult: (outcome, nodes, failure) => workflowRuntimeResult(context, outcome, nodes, failure),
|
|
@@ -89,6 +99,7 @@ async function runPipelineWithContext(context) {
|
|
|
89
99
|
at: now(),
|
|
90
100
|
type: "READY"
|
|
91
101
|
}),
|
|
102
|
+
resolveJournal: (nextContext) => resolveRunJournal(nextContext),
|
|
92
103
|
runWorkflowHook: (event, failure, nextContext) => dispatchHooks(nextContext, event, failure),
|
|
93
104
|
shouldContinueAfterNodeResult: (result, nextContext) => shouldContinueAfterNodeResult(result, nextContext),
|
|
94
105
|
skipNode: (nodeId, reason, nextContext) => recordSkippedNodeState(nextContext, nodeId, reason, now())
|
|
@@ -7,9 +7,14 @@ interface WorkflowPlannerIssue {
|
|
|
7
7
|
message: string;
|
|
8
8
|
path?: string;
|
|
9
9
|
}
|
|
10
|
-
declare
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
package/dist/planning/compile.js
CHANGED
|
@@ -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
|
|
6
|
-
code;
|
|
7
|
-
issues;
|
|
6
|
+
var WorkflowPlannerError = class extends Data.TaggedError("WorkflowPlannerError") {
|
|
8
7
|
constructor(code, message, issues = []) {
|
|
9
|
-
super(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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) {
|
|
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
|
|
|
43
43
|
declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
44
44
|
kind: z.ZodLiteral<"graph">;
|
|
45
45
|
mode: z.ZodEnum<{
|
|
46
|
-
full: "full";
|
|
47
46
|
quick: "quick";
|
|
47
|
+
full: "full";
|
|
48
48
|
}>;
|
|
49
49
|
}, z.core.$strict>, z.ZodObject<{
|
|
50
50
|
argv: z.ZodArray<z.ZodString>;
|
|
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
|
|
|
104
104
|
submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
105
105
|
kind: z.ZodLiteral<"graph">;
|
|
106
106
|
mode: z.ZodEnum<{
|
|
107
|
-
full: "full";
|
|
108
107
|
quick: "quick";
|
|
108
|
+
full: "full";
|
|
109
109
|
}>;
|
|
110
110
|
}, z.core.$strict>, z.ZodObject<{
|
|
111
111
|
argv: z.ZodArray<z.ZodString>;
|
|
@@ -140,8 +140,13 @@ interface RunnerCommandPayloadValidationIssue {
|
|
|
140
140
|
message: string;
|
|
141
141
|
path: string;
|
|
142
142
|
}
|
|
143
|
-
declare
|
|
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
|
|
75
|
-
issues;
|
|
75
|
+
var RunnerCommandPayloadValidationError = class extends Data.TaggedError("RunnerCommandPayloadValidationError") {
|
|
76
76
|
constructor(message, issues) {
|
|
77
|
-
super(
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
super({
|
|
78
|
+
message,
|
|
79
|
+
issues
|
|
80
|
+
});
|
|
80
81
|
}
|
|
81
82
|
};
|
|
82
83
|
function resolveRunnerEventSinkAuthToken(options) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mapRuntimeEventToRunnerEventRecords } from "./runner-command-contract.js";
|
|
2
|
+
import { Data } from "effect";
|
|
2
3
|
import ky, { isHTTPError } from "ky";
|
|
3
4
|
//#region src/runner-event-sink.ts
|
|
4
5
|
const DEFAULT_BATCH_SIZE = 50;
|
|
@@ -20,12 +21,12 @@ const RETRYABLE_STATUS_CODES = [
|
|
|
20
21
|
510,
|
|
21
22
|
511
|
|
22
23
|
];
|
|
23
|
-
var EventSinkHttpError = class extends
|
|
24
|
-
status;
|
|
24
|
+
var EventSinkHttpError = class extends Data.TaggedError("EventSinkHttpError") {
|
|
25
25
|
constructor(status, message) {
|
|
26
|
-
super(
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
super({
|
|
27
|
+
status,
|
|
28
|
+
message
|
|
29
|
+
});
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
function createRunnerEventSink(options) {
|
package/dist/runner.d.ts
CHANGED
|
@@ -80,7 +80,12 @@ interface RunnerLaunchInput {
|
|
|
80
80
|
prompt: string;
|
|
81
81
|
worktreePath: string;
|
|
82
82
|
}
|
|
83
|
-
declare
|
|
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
|
/**
|
package/dist/runner.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
1
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { execa } from "execa";
|
|
4
5
|
//#region src/runner.ts
|
|
5
|
-
var RunnerCapabilityError = class extends
|
|
6
|
+
var RunnerCapabilityError = class extends Data.TaggedError("RunnerCapabilityError") {
|
|
6
7
|
constructor(message) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "RunnerCapabilityError";
|
|
8
|
+
super({ message });
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
11
|
const OPENCODE_EXCLUDES = [
|
|
@@ -6,9 +6,10 @@ import { normalizeJsonSource, readJsonSchemaSource, validateJsonSchemaSource } f
|
|
|
6
6
|
import "../json-validation/index.js";
|
|
7
7
|
import { emit, emitAgentFinish, emitAgentStart } from "../events/events.js";
|
|
8
8
|
import "../events/index.js";
|
|
9
|
+
import { estimateTokens } from "../../token-estimator.js";
|
|
10
|
+
import { buildRepoMapContext } from "../../context/repo-map.js";
|
|
9
11
|
import { gatewayServerForProfile } from "../../mcp/gateway.js";
|
|
10
12
|
import { selectNodeModel } from "../../model-resolver.js";
|
|
11
|
-
import { estimateTokens } from "../../token-estimator.js";
|
|
12
13
|
import { handoffFinalizerPrompt, parseHandoff, renderHandoff, synthesizeMinimalHandoff } from "../handoff.js";
|
|
13
14
|
import { readFileSync } from "node:fs";
|
|
14
15
|
//#region src/runtime/agent-node/agent-node.ts
|
|
@@ -18,7 +19,7 @@ async function executeAgentNode(node, context, attempt) {
|
|
|
18
19
|
exitCode: 1,
|
|
19
20
|
output: ""
|
|
20
21
|
};
|
|
21
|
-
const prompt = renderAgentPrompt(node, context);
|
|
22
|
+
const prompt = await renderAgentPrompt(node, context);
|
|
22
23
|
const decision = decideNodeModel(prompt, node, context.config.token_budget);
|
|
23
24
|
if (decision.overBudget) return {
|
|
24
25
|
evidence: [
|
|
@@ -307,11 +308,28 @@ function createOutputRepairPlan(inputs) {
|
|
|
307
308
|
function normalizeAgentOutput(plan, stdout) {
|
|
308
309
|
return normalizeRunnerOutput(plan, stdout);
|
|
309
310
|
}
|
|
310
|
-
function
|
|
311
|
+
async function repoMapSection(node, context) {
|
|
312
|
+
const repoMap = context.config.repo_map;
|
|
313
|
+
if (!repoMap?.enabled) return "";
|
|
314
|
+
try {
|
|
315
|
+
return (await buildRepoMapContext({
|
|
316
|
+
artifacts: node.needs.flatMap((need) => context.nodeStateStore.handoff(need)?.artifacts ?? []),
|
|
317
|
+
taskText: context.task,
|
|
318
|
+
tokenBudget: repoMap.token_budget,
|
|
319
|
+
worktreePath: context.worktreePath
|
|
320
|
+
})).context;
|
|
321
|
+
} catch {
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function renderAgentPrompt(node, context) {
|
|
311
326
|
const profile = node.profile ? context.config.profiles[node.profile] : void 0;
|
|
327
|
+
const instructions = profile ? readInstructions(context.worktreePath, profile.instructions) : "";
|
|
328
|
+
const repoMap = await repoMapSection(node, context);
|
|
312
329
|
return [
|
|
313
|
-
|
|
330
|
+
instructions.trim(),
|
|
314
331
|
"",
|
|
332
|
+
repoMap,
|
|
315
333
|
`Task: ${context.task}`,
|
|
316
334
|
`Workflow: ${context.workflowId}`,
|
|
317
335
|
`Node: ${node.id}`,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { runWorkflowLifecycle } from "./workflow-lifecycle.js";
|
|
2
|
+
import { runWorkflowScheduler } from "./scheduler.js";
|
|
3
|
+
//#region src/runtime/local-scheduler.ts
|
|
4
|
+
/**
|
|
5
|
+
* The local, in-process scheduler seam. It owns workflow lifecycle hooks and
|
|
6
|
+
* delegates the topological fan-out/gating loop to `runWorkflowScheduler`,
|
|
7
|
+
* passing the durability journal (PIPE-83.10) through to it.
|
|
8
|
+
*/
|
|
9
|
+
var LocalScheduler = class {
|
|
10
|
+
options;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async runWorkflow(plan, context) {
|
|
15
|
+
const options = this.options;
|
|
16
|
+
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;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { LocalScheduler };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
1
2
|
import { createOpencode, createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
3
|
//#region src/runtime/opencode-server.ts
|
|
3
4
|
const DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
4
|
-
var OpencodeServerStartupError = class extends
|
|
5
|
+
var OpencodeServerStartupError = class extends Data.TaggedError("OpencodeServerStartupError") {
|
|
5
6
|
constructor(message, options) {
|
|
6
|
-
super(
|
|
7
|
-
|
|
7
|
+
super({
|
|
8
|
+
message,
|
|
9
|
+
cause: options?.cause
|
|
10
|
+
});
|
|
8
11
|
}
|
|
9
12
|
};
|
|
10
13
|
async function openOpencodeServer(options) {
|
|
@@ -1,60 +1,97 @@
|
|
|
1
1
|
import { childReporter } from "../events/events.js";
|
|
2
2
|
import "../events/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { WorktreeService, WorktreeServiceLive } from "../services/worktree-service.js";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
5
|
//#region src/runtime/parallel-node/parallel-node.ts
|
|
6
|
-
|
|
6
|
+
function executeParallelNode(node, context, runtime) {
|
|
7
|
+
return Effect.runPromise(Effect.provide(parallelNodeProgram(node, context, runtime), WorktreeServiceLive));
|
|
8
|
+
}
|
|
9
|
+
function parallelNodeProgram(node, context, runtime) {
|
|
7
10
|
const children = node.children ?? [];
|
|
8
|
-
if (children.length === 0) return {
|
|
11
|
+
if (children.length === 0) return Effect.succeed({
|
|
9
12
|
evidence: [`parallel node '${node.id}' has no children`],
|
|
10
13
|
exitCode: 1,
|
|
11
14
|
output: ""
|
|
15
|
+
});
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
yield* gcStaleWorktrees(context);
|
|
18
|
+
const failFast = context.plan.execution.failFast;
|
|
19
|
+
const linkedAbort = createLinkedAbortController(context.signal);
|
|
20
|
+
const childContext = createParallelChildContext(context, node.id, children, failFast ? linkedAbort.controller.signal : context.signal);
|
|
21
|
+
const gate = failFast ? makeFailFastGate(linkedAbort.controller) : void 0;
|
|
22
|
+
for (const child of children) runtime.markNodeReady(childContext, child.id);
|
|
23
|
+
const settled = yield* runAllChildren(children, childContext, runtime, yield* makeCategorySemaphores(childContext), gate).pipe(Effect.ensuring(Effect.sync(linkedAbort.cleanup)));
|
|
24
|
+
return aggregateParallelResult(node.id, children, settled);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function makeFailFastGate(controller) {
|
|
28
|
+
return {
|
|
29
|
+
abort: () => controller.abort(),
|
|
30
|
+
aborted: () => controller.signal.aborted
|
|
12
31
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
}
|
|
33
|
+
function aggregateParallelResult(nodeId, children, settled) {
|
|
34
|
+
const results = settled.filter((result) => result !== void 0);
|
|
35
|
+
const failed = results.filter((result) => result.status === "failed");
|
|
36
|
+
return {
|
|
37
|
+
evidence: parallelEvidence(nodeId, results, failed),
|
|
38
|
+
exitCode: failed.length > 0 ? 1 : 0,
|
|
39
|
+
output: parallelOutput(children, results)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function runAllChildren(children, context, runtime, caps, gate) {
|
|
43
|
+
return Effect.forEach(children, (child) => runChildCapped(child, context, runtime, caps, gate), { concurrency: context.maxParallelNodes ?? "unbounded" });
|
|
44
|
+
}
|
|
45
|
+
function runChildCapped(child, context, runtime, caps, gate) {
|
|
46
|
+
return Effect.gen(function* () {
|
|
47
|
+
if (gate?.aborted()) return;
|
|
48
|
+
const result = yield* withCategoryCap(caps, child.id, context, runChildInWorktree(child, context, runtime));
|
|
49
|
+
if (gate && result.status === "failed") gate.abort();
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function withCategoryCap(caps, childId, context, effect) {
|
|
54
|
+
const category = childCategory(childId, context.config.token_budget?.fan_out_width);
|
|
55
|
+
const semaphore = category ? caps.get(category) : void 0;
|
|
56
|
+
return semaphore ? semaphore.withPermits(1)(effect) : effect;
|
|
57
|
+
}
|
|
58
|
+
function makeCategorySemaphores(context) {
|
|
59
|
+
const fanOut = context.config.token_budget?.fan_out_width;
|
|
60
|
+
if (!fanOut) return Effect.succeed(/* @__PURE__ */ new Map());
|
|
61
|
+
return Effect.gen(function* () {
|
|
62
|
+
const caps = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const [category, permits] of Object.entries(fanOut.by_category)) caps.set(category, yield* Effect.makeSemaphore(permits));
|
|
64
|
+
return caps;
|
|
65
|
+
});
|
|
27
66
|
}
|
|
28
67
|
function gcStaleWorktrees(context) {
|
|
29
|
-
|
|
68
|
+
return Effect.gen(function* () {
|
|
69
|
+
if (context.config.parallel_worktrees?.enabled) yield* (yield* WorktreeService).gc(context.worktreePath);
|
|
70
|
+
});
|
|
30
71
|
}
|
|
31
72
|
/**
|
|
32
73
|
* PIPE-83.4: run a parallel child in its own git worktree when enabled, so
|
|
33
|
-
* concurrent candidate edits can't collide. The lease is
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* selection. Default-off path is byte-identical to the prior behaviour.
|
|
74
|
+
* concurrent candidate edits can't collide. The worktree lease is acquired and
|
|
75
|
+
* released as an Effect-scoped resource (released on success, failure, or
|
|
76
|
+
* interruption); release retains dirty/unpushed work for downstream selection.
|
|
37
77
|
*/
|
|
38
78
|
function runChildInWorktree(child, context, runtime) {
|
|
39
|
-
|
|
79
|
+
if (!context.config.parallel_worktrees?.enabled) return Effect.promise(() => runtime.executeNode(child, context));
|
|
80
|
+
return Effect.gen(function* () {
|
|
81
|
+
const worktree = yield* WorktreeService;
|
|
82
|
+
return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => Effect.promise(() => runtime.executeNode(child, {
|
|
83
|
+
...context,
|
|
84
|
+
worktreePath: lease.path
|
|
85
|
+
})), (lease) => Effect.sync(() => lease.release()));
|
|
86
|
+
});
|
|
40
87
|
}
|
|
41
|
-
function
|
|
42
|
-
return
|
|
88
|
+
function childLeaseOptions(child, context) {
|
|
89
|
+
return {
|
|
43
90
|
childNodeId: child.id,
|
|
44
91
|
parentNodeId: context.parentParallelNodeId ?? "parallel",
|
|
45
92
|
repoRoot: context.worktreePath,
|
|
46
93
|
...context.runId ? { runId: context.runId } : {}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function runInLease(child, context, runtime, lease) {
|
|
50
|
-
try {
|
|
51
|
-
return await runtime.executeNode(child, {
|
|
52
|
-
...context,
|
|
53
|
-
worktreePath: lease.path
|
|
54
|
-
});
|
|
55
|
-
} finally {
|
|
56
|
-
lease.release();
|
|
57
|
-
}
|
|
94
|
+
};
|
|
58
95
|
}
|
|
59
96
|
function createParallelChildContext(context, parentNodeId, children, signal) {
|
|
60
97
|
return {
|
|
@@ -94,44 +131,6 @@ function createLinkedAbortController(signal) {
|
|
|
94
131
|
function childCategory(childId, fanOut) {
|
|
95
132
|
return fanOut ? Object.keys(fanOut.by_category).find((category) => childId.includes(category)) : void 0;
|
|
96
133
|
}
|
|
97
|
-
function makeCategoryGate(context) {
|
|
98
|
-
const fanOut = context.config.token_budget?.fan_out_width;
|
|
99
|
-
const limits = /* @__PURE__ */ new Map();
|
|
100
|
-
return (childId, run) => {
|
|
101
|
-
const category = childCategory(childId, fanOut);
|
|
102
|
-
if (!(category && fanOut)) return run();
|
|
103
|
-
let limit = limits.get(category);
|
|
104
|
-
if (!limit) {
|
|
105
|
-
limit = pLimit(fanOut.by_category[category]);
|
|
106
|
-
limits.set(category, limit);
|
|
107
|
-
}
|
|
108
|
-
return limit(run);
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function executeParallelChildren(children, context, runtime) {
|
|
112
|
-
for (const child of children) runtime.markNodeReady(context, child.id);
|
|
113
|
-
const gate = makeCategoryGate(context);
|
|
114
|
-
const runChild = (child) => gate(child.id, () => runChildInWorktree(child, context, runtime));
|
|
115
|
-
if (!context.maxParallelNodes) return Promise.all(children.map((child) => runChild(child)));
|
|
116
|
-
const limit = pLimit(context.maxParallelNodes);
|
|
117
|
-
return Promise.all(children.map((child) => limit(() => runChild(child))));
|
|
118
|
-
}
|
|
119
|
-
async function executeFailFastParallelChildren(children, context, abortController, runtime) {
|
|
120
|
-
for (const child of children) runtime.markNodeReady(context, child.id);
|
|
121
|
-
const gate = makeCategoryGate(context);
|
|
122
|
-
const limit = pLimit({
|
|
123
|
-
concurrency: context.maxParallelNodes ?? children.length,
|
|
124
|
-
rejectOnClear: true
|
|
125
|
-
});
|
|
126
|
-
return (await Promise.allSettled(children.map((child) => limit(async () => {
|
|
127
|
-
const result = await gate(child.id, () => runChildInWorktree(child, context, runtime));
|
|
128
|
-
if (result.status === "failed") {
|
|
129
|
-
abortController.abort();
|
|
130
|
-
limit.clearQueue();
|
|
131
|
-
}
|
|
132
|
-
return result;
|
|
133
|
-
})))).flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
134
|
-
}
|
|
135
134
|
function parallelEvidence(nodeId, results, failed) {
|
|
136
135
|
if (failed.length === 0) return [`parallel node '${nodeId}' completed ${results.length} child nodes`];
|
|
137
136
|
return [`parallel node '${nodeId}' failed with ${failed.length} failed child nodes`, ...failed.flatMap((result) => result.evidence)];
|