@oisincoveney/pipeline 2.1.1 → 2.3.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 +80 -0
- package/defaults/pipeline.yaml +111 -0
- package/defaults/profiles.yaml +215 -0
- package/defaults/runners.yaml +24 -0
- package/dist/argo-graph.js +59 -27
- package/dist/argo-submit.d.ts +0 -1
- package/dist/argo-submit.js +11 -6
- package/dist/argo-workflow.d.ts +1 -2
- package/dist/argo-workflow.js +0 -2
- package/dist/claude-settings-config.js +44 -0
- package/dist/cli/program.js +12 -5
- package/dist/cli/submit-options.js +1 -2
- package/dist/cluster-doctor.js +0 -12
- package/dist/commands/pipeline-command.js +1 -1
- package/dist/config/defaults.js +7 -350
- package/dist/config/schemas.d.ts +9 -9
- package/dist/install-commands/claude-code.js +160 -0
- package/dist/install-commands/opencode.js +56 -39
- package/dist/install-commands/shared.js +32 -5
- package/dist/install-commands.js +26 -15
- package/dist/json-config-merge.js +47 -0
- package/dist/mcp/gateway.js +9 -1
- package/dist/moka-global-config.d.ts +0 -1
- package/dist/moka-global-config.js +0 -1
- package/dist/moka-submit.d.ts +8 -11
- package/dist/moka-submit.js +1 -4
- package/dist/opencode-project-config.js +2 -45
- package/dist/pipeline-runtime.d.ts +9 -0
- package/dist/pipeline-runtime.js +50 -8
- package/dist/planned-node.js +2 -5
- package/dist/{workflow-planner.d.ts → planning/compile.d.ts} +2 -2
- package/dist/{workflow-planner.js → planning/compile.js} +6 -83
- package/dist/{schedule/planner.d.ts → planning/generate.d.ts} +17 -3
- package/dist/{schedule/planner.js → planning/generate.js} +24 -56
- package/dist/planning/graph.js +138 -0
- package/dist/runner-command/lifecycle-context.js +2 -3
- package/dist/runner-command/run.js +2 -3
- package/dist/runner-command-contract.d.ts +2 -2
- package/dist/runner-event-schema.d.ts +349 -0
- package/dist/runner-event-schema.js +185 -0
- package/dist/runner-output.js +2 -2
- package/dist/runner.d.ts +29 -0
- package/dist/runtime/agent-node/agent-node.js +1 -0
- package/dist/runtime/context/context.js +1 -1
- package/dist/runtime/contracts/contracts.d.ts +18 -1
- package/dist/runtime/node-state-store.js +7 -0
- package/dist/runtime/opencode-adapter.js +28 -8
- package/dist/runtime/opencode-agent-name.js +18 -0
- package/dist/runtime/opencode-runtime.js +62 -0
- package/dist/runtime/opencode-server.js +67 -0
- package/dist/runtime/opencode-session-executor.js +206 -0
- package/dist/schedule/passes/coverage.js +7 -51
- package/dist/schedule/passes/ids.js +3 -23
- package/dist/schedule/scheduling-roles.js +19 -0
- package/dist/strings.js +30 -1
- package/docs/adr-opencode-first-goal-loop-runtime.md +1 -1
- package/docs/config-architecture.md +43 -6
- package/docs/mcp-gateway.md +4 -4
- package/docs/operator-guide.md +9 -8
- package/docs/pipeline-console-runner-contract.md +3 -4
- package/docs/slash-command-adapter-contract.md +1 -0
- package/package.json +10 -5
- package/dist/schedule-planner.d.ts +0 -2
- package/dist/schedule-planner.js +0 -2
|
@@ -1,36 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyJsonEdit, ensureTrailingNewline, formatJson, parseJsonRecord, setIfMissing } from "./json-config-merge.js";
|
|
2
2
|
//#region src/opencode-project-config.ts
|
|
3
|
-
const JSON_FORMAT_OPTIONS = {
|
|
4
|
-
insertSpaces: true,
|
|
5
|
-
tabSize: 2
|
|
6
|
-
};
|
|
7
3
|
function mergeOpenCodeProjectConfig(currentText, projection) {
|
|
8
4
|
if (currentText === void 0) return {
|
|
9
5
|
content: formatJson(projection),
|
|
10
6
|
ok: true
|
|
11
7
|
};
|
|
12
|
-
const parsed =
|
|
8
|
+
const parsed = parseJsonRecord(currentText);
|
|
13
9
|
if (!parsed.ok) return parsed;
|
|
14
10
|
return {
|
|
15
11
|
content: ensureTrailingNewline(applyOpenCodeProjection(currentText, parsed.value, projection)),
|
|
16
12
|
ok: true
|
|
17
13
|
};
|
|
18
14
|
}
|
|
19
|
-
function parseOpenCodeProjectConfig(currentText) {
|
|
20
|
-
const errors = [];
|
|
21
|
-
const value = parse(currentText, errors, {
|
|
22
|
-
allowTrailingComma: true,
|
|
23
|
-
disallowComments: false
|
|
24
|
-
});
|
|
25
|
-
if (errors.length > 0 || !isRecord(value)) return {
|
|
26
|
-
errors,
|
|
27
|
-
ok: false
|
|
28
|
-
};
|
|
29
|
-
return {
|
|
30
|
-
ok: true,
|
|
31
|
-
value
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
15
|
function applyOpenCodeProjection(currentText, parsed, projection) {
|
|
35
16
|
return applyProviderProjection(applyPluginProjection(applyMcpProjection(setIfMissing(setIfMissing(currentText, parsed, ["$schema"], projection.$schema), parsed, ["lsp"], projection.lsp), parsed, projection), parsed, projection), parsed, projection);
|
|
36
17
|
}
|
|
@@ -49,18 +30,6 @@ function applyPluginProjection(content, parsed, projection) {
|
|
|
49
30
|
const plugins = mergePluginEntries(parsed.plugin, projection.plugin ?? []);
|
|
50
31
|
return plugins.length > 0 ? applyJsonEdit(content, ["plugin"], plugins) : content;
|
|
51
32
|
}
|
|
52
|
-
function setIfMissing(content, parsed, path, value) {
|
|
53
|
-
if (value === void 0 || hasPath(parsed, path)) return content;
|
|
54
|
-
return applyJsonEdit(content, path, value);
|
|
55
|
-
}
|
|
56
|
-
function hasPath(value, path) {
|
|
57
|
-
let cursor = value;
|
|
58
|
-
for (const segment of path) {
|
|
59
|
-
if (!(isRecord(cursor) && segment in cursor)) return false;
|
|
60
|
-
cursor = cursor[segment];
|
|
61
|
-
}
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
33
|
function mergePluginEntries(existing, projected) {
|
|
65
34
|
const projectedByKey = new Map(projected.map((plugin) => [pluginKey(plugin), plugin]));
|
|
66
35
|
const merged = [];
|
|
@@ -86,17 +55,5 @@ function pluginName(specifier) {
|
|
|
86
55
|
const versionSeparator = specifier.indexOf("@", 1);
|
|
87
56
|
return versionSeparator === -1 ? specifier : specifier.slice(0, versionSeparator);
|
|
88
57
|
}
|
|
89
|
-
function applyJsonEdit(content, path, value) {
|
|
90
|
-
return applyEdits(content, modify(content, path, value, { formattingOptions: JSON_FORMAT_OPTIONS }));
|
|
91
|
-
}
|
|
92
|
-
function formatJson(value) {
|
|
93
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
94
|
-
}
|
|
95
|
-
function ensureTrailingNewline(value) {
|
|
96
|
-
return value.endsWith("\n") ? value : `${value}\n`;
|
|
97
|
-
}
|
|
98
|
-
function isRecord(value) {
|
|
99
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
-
}
|
|
101
58
|
//#endregion
|
|
102
59
|
export { mergeOpenCodeProjectConfig };
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { PipelineConfigError } from "./config/schemas.js";
|
|
2
2
|
import { AcceptanceCriterion, HookRuntimePolicy, NodeExecutionState, NodeStatus, PipelineRuntimeEvent, PipelineRuntimeObservabilityLevel, PipelineRuntimeOptions, PipelineRuntimeResult, PipelineTaskContext, RuntimeFailure, RuntimeGateResult, RuntimeNodeResult, RuntimeStructuredOutput } from "./runtime/contracts/contracts.js";
|
|
3
3
|
//#region src/pipeline-runtime.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Top layer of the runtime-options stack (PIPE-74 B3). Extends
|
|
6
|
+
* {@link PipelineRuntimeOptions} for the schedule-driven path that runs a
|
|
7
|
+
* SINGLE workflow node (`nodeId`) in isolation, supplying that node's upstream
|
|
8
|
+
* `dependencyOutputs`. Full stack:
|
|
9
|
+
* RunnerExecutionOptions (src/runner.ts)
|
|
10
|
+
* < PipelineRuntimeOptions (src/runtime/contracts/contracts.ts)
|
|
11
|
+
* < ScheduledWorkflowTaskRuntimeOptions (this type)
|
|
12
|
+
*/
|
|
4
13
|
interface ScheduledWorkflowTaskRuntimeOptions extends PipelineRuntimeOptions {
|
|
5
14
|
dependencyOutputs?: Map<string, string> | Record<string, string>;
|
|
6
15
|
nodeId: string;
|
package/dist/pipeline-runtime.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { loadPipelineConfig } from "./config/load.js";
|
|
2
|
+
import "./config.js";
|
|
1
3
|
import { executeCommand } from "./runtime/command-executor/command-executor.js";
|
|
2
4
|
import "./runtime/command-executor/index.js";
|
|
3
5
|
import { parseJsonObject } from "./runtime/json-validation/json-validation.js";
|
|
@@ -17,24 +19,64 @@ import { diffChangedFiles, snapshotChangedFiles } from "./runtime/changed-files/
|
|
|
17
19
|
import { evaluateNodeGates } from "./runtime/gates/gates.js";
|
|
18
20
|
import "./runtime/gates/index.js";
|
|
19
21
|
import { NodeStateTracker } from "./runtime/node-state-tracker.js";
|
|
22
|
+
import { configUsesOpencode, leaseOpencodeRuntime } from "./runtime/opencode-runtime.js";
|
|
20
23
|
import { executeParallelNode } from "./runtime/parallel-node/parallel-node.js";
|
|
21
24
|
import "./runtime/parallel-node/index.js";
|
|
22
25
|
import { decideNodeRetry, nodeRetryPolicy } from "./runtime/retry.js";
|
|
23
26
|
import { LocalScheduler } from "./runtime/scheduler.js";
|
|
24
27
|
//#region src/pipeline-runtime.ts
|
|
25
28
|
function runPipelineFromConfig(options) {
|
|
26
|
-
return runPipelineWithContext(createRuntimeContext(
|
|
29
|
+
return withOpencodeRuntime(options, (resolved) => runPipelineWithContext(createRuntimeContext(resolved)));
|
|
27
30
|
}
|
|
28
31
|
function runScheduledWorkflowTask(options) {
|
|
29
32
|
const { dependencyOutputs, nodeId, ...runtimeOptions } = options;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
return withOpencodeRuntime(runtimeOptions, (resolved) => {
|
|
34
|
+
const context = createRuntimeContext(resolved);
|
|
35
|
+
hydrateScheduledDependencyStates(context, nodeId);
|
|
36
|
+
hydrateDependencyOutputs(context, dependencyOutputs);
|
|
37
|
+
recordNodeEvent(context, nodeId, {
|
|
38
|
+
at: now(),
|
|
39
|
+
type: "READY"
|
|
40
|
+
});
|
|
41
|
+
return executePlannedNode(nodeId, context);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* When the config uses opencode and the caller did not inject an executor,
|
|
46
|
+
* open one opencode server for the run, drive nodes through the SDK executor,
|
|
47
|
+
* and tear the server down afterward. Command-only configs and callers that
|
|
48
|
+
* supply their own executor (tests, embedders) are passed through untouched.
|
|
49
|
+
*/
|
|
50
|
+
async function withOpencodeRuntime(options, run) {
|
|
51
|
+
if (options.executor) return await run(options);
|
|
52
|
+
const { config, worktreePath } = resolveConfigForRun(options);
|
|
53
|
+
return configUsesOpencode(config) ? await runWithLeasedOpencode(options, config, worktreePath, run) : await run({
|
|
54
|
+
...options,
|
|
55
|
+
config
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function resolveConfigForRun(options) {
|
|
59
|
+
const worktreePath = options.worktreePath ?? process.cwd();
|
|
60
|
+
return {
|
|
61
|
+
config: options.config ?? loadPipelineConfig(worktreePath),
|
|
62
|
+
worktreePath
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function runWithLeasedOpencode(options, config, worktreePath, run) {
|
|
66
|
+
const lease = await leaseOpencodeRuntime({
|
|
67
|
+
config,
|
|
68
|
+
...options.signal ? { signal: options.signal } : {},
|
|
69
|
+
worktreePath
|
|
36
70
|
});
|
|
37
|
-
|
|
71
|
+
try {
|
|
72
|
+
return await run({
|
|
73
|
+
...options,
|
|
74
|
+
config,
|
|
75
|
+
executor: lease.executor
|
|
76
|
+
});
|
|
77
|
+
} finally {
|
|
78
|
+
await lease.release();
|
|
79
|
+
}
|
|
38
80
|
}
|
|
39
81
|
async function runPipelineWithContext(context) {
|
|
40
82
|
return finishRuntime(context, await new LocalScheduler({
|
package/dist/planned-node.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
+
import { findNode } from "./planning/graph.js";
|
|
1
2
|
//#region src/planned-node.ts
|
|
2
3
|
function findPlannedNode(nodes, nodeId) {
|
|
3
|
-
|
|
4
|
-
if (node.id === nodeId) return node;
|
|
5
|
-
const child = findPlannedNode(node.children ?? [], nodeId);
|
|
6
|
-
if (child) return child;
|
|
7
|
-
}
|
|
4
|
+
return findNode(nodes, nodeId, (node) => node.children);
|
|
8
5
|
}
|
|
9
6
|
//#endregion
|
|
10
7
|
export { findPlannedNode };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { PipelineConfig, WorkflowNodeKind } from "
|
|
1
|
+
import { PipelineConfig, WorkflowNodeKind } from "../config/schemas.js";
|
|
2
2
|
import { Graph } from "@dagrejs/graphlib";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/planning/compile.d.ts
|
|
5
5
|
type WorkflowPlannerErrorCode = "WORKFLOW_CYCLE" | "WORKFLOW_DUPLICATE_NODE" | "WORKFLOW_GROUP_REFERENCE" | "WORKFLOW_MISSING_DEPENDENCY" | "WORKFLOW_MISSING_WORKFLOW";
|
|
6
6
|
interface WorkflowPlannerIssue {
|
|
7
7
|
message: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { uniqueStrings } from "
|
|
1
|
+
import { uniqueStrings } from "../strings.js";
|
|
2
|
+
import { findDependencyCycles } from "./graph.js";
|
|
2
3
|
import { Graph } from "@dagrejs/graphlib";
|
|
3
|
-
//#region src/
|
|
4
|
+
//#region src/planning/compile.ts
|
|
4
5
|
var WorkflowPlannerError = class extends Error {
|
|
5
6
|
code;
|
|
6
7
|
issues;
|
|
@@ -54,7 +55,7 @@ function validateNodeGraph(workflowId, nodes) {
|
|
|
54
55
|
...groupIssues(workflowId, nodes, nodeIds),
|
|
55
56
|
...dependencyIssues(workflowId, nodes, nodeIds)
|
|
56
57
|
];
|
|
57
|
-
if (duplicateIssues.length === 0) return [...issues, ...cycleIssues(workflowId, nodes
|
|
58
|
+
if (duplicateIssues.length === 0) return [...issues, ...cycleIssues(workflowId, nodes)];
|
|
58
59
|
return issues;
|
|
59
60
|
}
|
|
60
61
|
function duplicateNodeIssues(workflowId, nodes) {
|
|
@@ -100,92 +101,14 @@ function groupChildIssues(workflowId, node, nodeIds) {
|
|
|
100
101
|
function isGroupNode(node) {
|
|
101
102
|
return node.kind === "group";
|
|
102
103
|
}
|
|
103
|
-
function cycleIssues(workflowId, nodes
|
|
104
|
-
return
|
|
104
|
+
function cycleIssues(workflowId, nodes) {
|
|
105
|
+
return findDependencyCycles(nodes).map((cycle) => {
|
|
105
106
|
return {
|
|
106
107
|
path: `workflows.${workflowId}.nodes.${cycle[0] ?? "nodes"}.needs`,
|
|
107
108
|
message: `workflow '${workflowId}' contains dependency cycle: ${cycle.join(" -> ")}`
|
|
108
109
|
};
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
|
-
function dependencyCycles(nodes, nodeIds) {
|
|
112
|
-
const dependentsByNeed = dependentsByNeedMap(nodes, nodeIds);
|
|
113
|
-
const state = /* @__PURE__ */ new Map();
|
|
114
|
-
const path = [];
|
|
115
|
-
const pathIndex = /* @__PURE__ */ new Map();
|
|
116
|
-
const cycles = [];
|
|
117
|
-
const cycleKeys = /* @__PURE__ */ new Set();
|
|
118
|
-
for (const node of nodes) {
|
|
119
|
-
if (state.has(node.id)) continue;
|
|
120
|
-
visitForCycles(node.id, {
|
|
121
|
-
cycleKeys,
|
|
122
|
-
cycles,
|
|
123
|
-
dependentsByNeed,
|
|
124
|
-
path,
|
|
125
|
-
pathIndex,
|
|
126
|
-
state
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return cycles;
|
|
130
|
-
}
|
|
131
|
-
function visitForCycles(startId, visitState) {
|
|
132
|
-
const frames = [{
|
|
133
|
-
index: 0,
|
|
134
|
-
nodeId: startId
|
|
135
|
-
}];
|
|
136
|
-
markVisiting(startId, visitState);
|
|
137
|
-
while (frames.length > 0) {
|
|
138
|
-
const frame = frames.at(-1);
|
|
139
|
-
if (!frame) return;
|
|
140
|
-
const dependentId = (visitState.dependentsByNeed.get(frame.nodeId) ?? [])[frame.index];
|
|
141
|
-
if (!dependentId) {
|
|
142
|
-
markDone(frame.nodeId, visitState);
|
|
143
|
-
frames.pop();
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
frame.index += 1;
|
|
147
|
-
const dependentState = visitState.state.get(dependentId);
|
|
148
|
-
if (dependentState === "visiting") {
|
|
149
|
-
recordCycle(dependentId, visitState);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (dependentState === "done") continue;
|
|
153
|
-
markVisiting(dependentId, visitState);
|
|
154
|
-
frames.push({
|
|
155
|
-
index: 0,
|
|
156
|
-
nodeId: dependentId
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function markVisiting(nodeId, visitState) {
|
|
161
|
-
visitState.state.set(nodeId, "visiting");
|
|
162
|
-
visitState.pathIndex.set(nodeId, visitState.path.length);
|
|
163
|
-
visitState.path.push(nodeId);
|
|
164
|
-
}
|
|
165
|
-
function markDone(nodeId, visitState) {
|
|
166
|
-
visitState.state.set(nodeId, "done");
|
|
167
|
-
visitState.pathIndex.delete(nodeId);
|
|
168
|
-
visitState.path.pop();
|
|
169
|
-
}
|
|
170
|
-
function recordCycle(nodeId, visitState) {
|
|
171
|
-
const startIndex = visitState.pathIndex.get(nodeId);
|
|
172
|
-
if (startIndex === void 0) return;
|
|
173
|
-
const cycle = visitState.path.slice(startIndex);
|
|
174
|
-
const key = [...cycle].sort().join("\0");
|
|
175
|
-
if (visitState.cycleKeys.has(key)) return;
|
|
176
|
-
visitState.cycleKeys.add(key);
|
|
177
|
-
visitState.cycles.push(cycle);
|
|
178
|
-
}
|
|
179
|
-
function dependentsByNeedMap(nodes, nodeIds) {
|
|
180
|
-
const dependentsByNeed = /* @__PURE__ */ new Map();
|
|
181
|
-
for (const node of nodes) for (const need of uniqueStrings(node.needs ?? [])) {
|
|
182
|
-
if (!nodeIds.has(need)) continue;
|
|
183
|
-
const dependents = dependentsByNeed.get(need) ?? [];
|
|
184
|
-
dependents.push(node.id);
|
|
185
|
-
dependentsByNeed.set(need, dependents);
|
|
186
|
-
}
|
|
187
|
-
return dependentsByNeed;
|
|
188
|
-
}
|
|
189
112
|
function topologicalOrderForPlan(graph) {
|
|
190
113
|
const visited = /* @__PURE__ */ new Set();
|
|
191
114
|
const inStack = /* @__PURE__ */ new Set();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PipelineConfig } from "../config/schemas.js";
|
|
2
|
-
import { WorkflowExecutionPlan } from "
|
|
2
|
+
import { WorkflowExecutionPlan } from "./compile.js";
|
|
3
3
|
import { AgentResult, RunnerExecutionOptions, RunnerLaunchPlan } from "../runner.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
-
//#region src/
|
|
6
|
+
//#region src/planning/generate.d.ts
|
|
7
7
|
declare const scheduleArtifactSchema: z.ZodObject<{
|
|
8
8
|
generated_at: z.ZodString;
|
|
9
9
|
kind: z.ZodLiteral<"pipeline-schedule">;
|
|
@@ -1575,6 +1575,20 @@ interface GenerateScheduleResult {
|
|
|
1575
1575
|
artifact: ScheduleArtifact;
|
|
1576
1576
|
path: string;
|
|
1577
1577
|
}
|
|
1578
|
+
interface BacklogWorkUnit {
|
|
1579
|
+
acceptance_criteria: Array<{
|
|
1580
|
+
id: string;
|
|
1581
|
+
text: string;
|
|
1582
|
+
}>;
|
|
1583
|
+
dependencies?: string[];
|
|
1584
|
+
description?: string;
|
|
1585
|
+
id: string;
|
|
1586
|
+
title?: string;
|
|
1587
|
+
}
|
|
1588
|
+
interface SchedulePlanningContext {
|
|
1589
|
+
parentWorkUnits: BacklogWorkUnit[];
|
|
1590
|
+
workUnits: BacklogWorkUnit[];
|
|
1591
|
+
}
|
|
1578
1592
|
declare function parseScheduleArtifact(source: string, sourcePath?: string): ScheduleArtifact;
|
|
1579
1593
|
declare class ScheduleArtifactError extends Error {
|
|
1580
1594
|
constructor(message: string);
|
|
@@ -1583,4 +1597,4 @@ declare function compileScheduleArtifact(config: PipelineConfig, artifact: Sched
|
|
|
1583
1597
|
declare function generateScheduleArtifact(options: GenerateScheduleOptions): Promise<GenerateScheduleResult>;
|
|
1584
1598
|
declare function scheduleArtifactPath(worktreePath: string, scheduleId: string): string;
|
|
1585
1599
|
//#endregion
|
|
1586
|
-
export { CompiledScheduleArtifact, GenerateScheduleOptions, GenerateScheduleResult, ScheduleArtifact, ScheduleArtifactError, compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact, scheduleArtifactPath };
|
|
1600
|
+
export { BacklogWorkUnit, CompiledScheduleArtifact, GenerateScheduleOptions, GenerateScheduleResult, ScheduleArtifact, ScheduleArtifactError, SchedulePlanningContext, compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact, scheduleArtifactPath };
|
|
@@ -3,20 +3,22 @@ import { validatePipelineConfig } from "../config/validate.js";
|
|
|
3
3
|
import "../config.js";
|
|
4
4
|
import { createRunnerLaunchPlan, runLaunchPlan } from "../runner.js";
|
|
5
5
|
import { normalizeRunnerOutput } from "../runner-output.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
6
|
+
import { loadBacklogPlanningContext } from "../schedule/backlog-context.js";
|
|
7
|
+
import { baselineScheduleArtifact } from "../schedule/baseline.js";
|
|
8
|
+
import { dependentsByNeed, flattenNodes, hasReachableDependent } from "./graph.js";
|
|
9
|
+
import { isCoverageNode, isImplementationNode } from "../schedule/scheduling-roles.js";
|
|
10
|
+
import { addGeneratedImplementationCoverage } from "../schedule/passes/coverage.js";
|
|
11
|
+
import { canonicalizeGeneratedScheduleIds } from "../schedule/passes/ids.js";
|
|
12
|
+
import { SCHEDULE_PASS_ORDER } from "../schedule/passes/index.js";
|
|
13
|
+
import { applyNodeCatalogModelFallbacks } from "../schedule/passes/models.js";
|
|
14
|
+
import { namespaceScheduleWorkflows } from "../schedule/passes/references.js";
|
|
15
|
+
import { plannerPrompt, plannerRepairPrompt } from "../schedule/prompts.js";
|
|
16
|
+
import { compileWorkflowPlan } from "./compile.js";
|
|
15
17
|
import { parseDocument, stringify } from "yaml";
|
|
16
18
|
import { z } from "zod";
|
|
17
19
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
18
20
|
import { join } from "node:path";
|
|
19
|
-
//#region src/
|
|
21
|
+
//#region src/planning/generate.ts
|
|
20
22
|
const SCHEDULE_KIND = "pipeline-schedule";
|
|
21
23
|
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
22
24
|
const SCHEDULE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
@@ -253,9 +255,9 @@ function unsafeParallelWorktreeIssues(config, artifact) {
|
|
|
253
255
|
}
|
|
254
256
|
function workflowNodeIssues(artifact, collectIssues) {
|
|
255
257
|
return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
|
|
256
|
-
const nodes = workflow.nodes
|
|
258
|
+
const nodes = flattenWorkflowNodes(workflow.nodes);
|
|
257
259
|
return collectIssues({
|
|
258
|
-
dependentsByNeed:
|
|
260
|
+
dependentsByNeed: dependentsByNeed(nodes),
|
|
259
261
|
nodes,
|
|
260
262
|
workflowId
|
|
261
263
|
});
|
|
@@ -267,20 +269,8 @@ function isWriteCapableParallelChild(config, node) {
|
|
|
267
269
|
if (node.kind === "parallel") return node.nodes.some((child) => isWriteCapableParallelChild(config, child));
|
|
268
270
|
return false;
|
|
269
271
|
}
|
|
270
|
-
function hasDownstreamDrainMerge(nodeId,
|
|
271
|
-
return hasReachableDependent(nodeId,
|
|
272
|
-
}
|
|
273
|
-
function hasReachableDependent(nodeId, dependentsByNeed, matches) {
|
|
274
|
-
const queue = [...dependentsByNeed.get(nodeId) ?? []];
|
|
275
|
-
const seen = /* @__PURE__ */ new Set();
|
|
276
|
-
while (queue.length > 0) {
|
|
277
|
-
const node = queue.shift();
|
|
278
|
-
if (!node || seen.has(node.id)) continue;
|
|
279
|
-
seen.add(node.id);
|
|
280
|
-
if (matches(node)) return true;
|
|
281
|
-
queue.push(...dependentsByNeed.get(node.id) ?? []);
|
|
282
|
-
}
|
|
283
|
-
return false;
|
|
272
|
+
function hasDownstreamDrainMerge(nodeId, index) {
|
|
273
|
+
return hasReachableDependent(nodeId, index, (node) => node.kind === "builtin" && node.builtin === "drain-merge");
|
|
284
274
|
}
|
|
285
275
|
function generatedRootWorkflowIssues(artifact) {
|
|
286
276
|
const workflowIds = Object.keys(artifact.workflows);
|
|
@@ -331,15 +321,15 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
|
|
|
331
321
|
const workUnitIds = new Set(workUnits.map((unit) => unit.id));
|
|
332
322
|
const dependenciesByUnit = new Map(workUnits.map((unit) => [unit.id, (unit.dependencies ?? []).filter((id) => workUnitIds.has(id))]));
|
|
333
323
|
return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
|
|
334
|
-
const nodes = workflow.nodes
|
|
335
|
-
const
|
|
324
|
+
const nodes = flattenWorkflowNodes(workflow.nodes);
|
|
325
|
+
const index = dependentsByNeed(nodes);
|
|
336
326
|
const nodesByWorkUnit = nodesByAssignedWorkUnit(nodes);
|
|
337
327
|
return nodes.filter((node) => isImplementationNode(config, node)).flatMap((node) => {
|
|
338
328
|
const dependentId = node.task_context?.id;
|
|
339
329
|
if (!dependentId) return [];
|
|
340
330
|
return (dependenciesByUnit.get(dependentId) ?? []).flatMap((prerequisiteId) => {
|
|
341
331
|
const prerequisiteNodes = nodesByWorkUnit.get(prerequisiteId) ?? [];
|
|
342
|
-
return prerequisiteNodes.some((source) =>
|
|
332
|
+
return prerequisiteNodes.some((source) => hasReachableDependent(source.id, index, (candidate) => candidate.id === node.id)) ? [] : [`work unit dependency edge missing in '${workflowId}': '${dependentId}' node '${node.id}' must depend on prerequisite '${prerequisiteId}' nodes ${prerequisiteNodes.map((prerequisite) => `'${prerequisite.id}'`).join(", ")}`];
|
|
343
333
|
});
|
|
344
334
|
});
|
|
345
335
|
});
|
|
@@ -355,9 +345,6 @@ function nodesByAssignedWorkUnit(nodes) {
|
|
|
355
345
|
}
|
|
356
346
|
return grouped;
|
|
357
347
|
}
|
|
358
|
-
function hasPathToNode(sourceId, targetId, dependentsByNeed) {
|
|
359
|
-
return hasReachableDependent(sourceId, dependentsByNeed, (node) => node.id === targetId);
|
|
360
|
-
}
|
|
361
348
|
function unsupportedGeneratedBuiltinIssues(artifact) {
|
|
362
349
|
const allowed = new Set(SCHEDULE_BUILTINS);
|
|
363
350
|
return allWorkflowNodes(artifact.workflows).flatMap((node) => {
|
|
@@ -369,33 +356,14 @@ function unsupportedGeneratedBuiltinIssues(artifact) {
|
|
|
369
356
|
function implementationCoverageIssues(config, artifact) {
|
|
370
357
|
return workflowNodeIssues(artifact, ({ dependentsByNeed, nodes, workflowId }) => nodes.filter((node) => isImplementationNode(config, node)).filter((node) => !hasDownstreamCoverage(config, node.id, dependentsByNeed)).map((node) => `implementation node '${workflowId}.${node.id}' is without downstream verification or review`));
|
|
371
358
|
}
|
|
372
|
-
function
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
function workflowDependentsByNeed(nodes) {
|
|
376
|
-
const dependentsByNeed = /* @__PURE__ */ new Map();
|
|
377
|
-
for (const node of nodes) for (const need of node.needs ?? []) {
|
|
378
|
-
const dependents = dependentsByNeed.get(need) ?? [];
|
|
379
|
-
dependents.push(node);
|
|
380
|
-
dependentsByNeed.set(need, dependents);
|
|
381
|
-
}
|
|
382
|
-
return dependentsByNeed;
|
|
383
|
-
}
|
|
384
|
-
function hasDownstreamCoverage(config, nodeId, dependentsByNeed) {
|
|
385
|
-
return hasReachableDependent(nodeId, dependentsByNeed, (node) => isCoverageNode(config, node));
|
|
386
|
-
}
|
|
387
|
-
function isCoverageNode(config, node) {
|
|
388
|
-
return hasSchedulingRole(config, node, "coverage");
|
|
389
|
-
}
|
|
390
|
-
function hasSchedulingRole(config, node, role) {
|
|
391
|
-
if (node.kind !== "agent") return false;
|
|
392
|
-
return config.profiles[node.profile]?.scheduling_roles?.includes(role) ?? false;
|
|
359
|
+
function hasDownstreamCoverage(config, nodeId, index) {
|
|
360
|
+
return hasReachableDependent(nodeId, index, (node) => isCoverageNode(config, node));
|
|
393
361
|
}
|
|
394
362
|
function allWorkflowNodes(workflows) {
|
|
395
|
-
return Object.values(workflows).flatMap((workflow) => workflow.nodes
|
|
363
|
+
return Object.values(workflows).flatMap((workflow) => flattenWorkflowNodes(workflow.nodes));
|
|
396
364
|
}
|
|
397
|
-
function
|
|
398
|
-
return node.kind === "parallel" ?
|
|
365
|
+
function flattenWorkflowNodes(nodes) {
|
|
366
|
+
return flattenNodes(nodes, (node) => node.kind === "parallel" ? node.nodes : void 0);
|
|
399
367
|
}
|
|
400
368
|
//#endregion
|
|
401
369
|
export { ScheduleArtifactError, compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact, scheduleArtifactPath };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/planning/graph.ts
|
|
2
|
+
/**
|
|
3
|
+
* Depth-first flatten of a node tree into a flat list (parents before
|
|
4
|
+
* children), using the caller-supplied accessor to descend into nested
|
|
5
|
+
* containers (e.g. parallel children). Nodes without children are returned
|
|
6
|
+
* as-is.
|
|
7
|
+
*/
|
|
8
|
+
function flattenNodes(nodes, childrenOf) {
|
|
9
|
+
return nodes.flatMap((node) => {
|
|
10
|
+
const children = childrenOf(node);
|
|
11
|
+
return children && children.length > 0 ? [node, ...flattenNodes(children, childrenOf)] : [node];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build an index mapping each `need` id to the (flat) nodes that declare it as a
|
|
16
|
+
* dependency. The input is expected to already be flattened by the caller so
|
|
17
|
+
* that the resulting index spans the whole graph.
|
|
18
|
+
*/
|
|
19
|
+
function dependentsByNeed(nodes) {
|
|
20
|
+
const index = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const node of nodes) for (const need of node.needs ?? []) {
|
|
22
|
+
const dependents = index.get(need) ?? [];
|
|
23
|
+
dependents.push(node);
|
|
24
|
+
index.set(need, dependents);
|
|
25
|
+
}
|
|
26
|
+
return index;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Breadth-first search over the dependents index to determine whether any node
|
|
30
|
+
* transitively reachable downstream of `nodeId` satisfies `matches`. Cycle-safe
|
|
31
|
+
* via a visited set.
|
|
32
|
+
*/
|
|
33
|
+
function hasReachableDependent(nodeId, index, matches) {
|
|
34
|
+
const visited = /* @__PURE__ */ new Set();
|
|
35
|
+
const queue = [...index.get(nodeId) ?? []];
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const node = queue.shift();
|
|
38
|
+
if (!node || visited.has(node.id)) continue;
|
|
39
|
+
visited.add(node.id);
|
|
40
|
+
if (matches(node)) return true;
|
|
41
|
+
queue.push(...index.get(node.id) ?? []);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Recursively find a node by id within a node tree, descending into children
|
|
47
|
+
* via the caller-supplied accessor. Returns `undefined` when absent.
|
|
48
|
+
*/
|
|
49
|
+
function findNode(nodes, nodeId, childrenOf) {
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
if (node.id === nodeId) return node;
|
|
52
|
+
const child = findNode(childrenOf(node) ?? [], nodeId, childrenOf);
|
|
53
|
+
if (child) return child;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Find all dependency cycles in a flat node list using an iterative DFS over the
|
|
58
|
+
* dependents-by-need graph. Each cycle is returned once (canonicalized by its
|
|
59
|
+
* member set) as the ordered list of node ids forming the loop. Only `needs`
|
|
60
|
+
* that reference declared nodes participate, so callers can pass the full flat
|
|
61
|
+
* node set without pre-filtering missing dependencies.
|
|
62
|
+
*
|
|
63
|
+
* The traversal is iterative (an explicit frame stack) rather than recursive so
|
|
64
|
+
* that deep generated workflow chains cannot overflow the call stack.
|
|
65
|
+
*/
|
|
66
|
+
function findDependencyCycles(nodes) {
|
|
67
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
68
|
+
const index = dependentsByNeed(nodes);
|
|
69
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const node of nodes) adjacency.set(node.id, (index.get(node.id) ?? []).map((dependent) => dependent.id).filter((id) => nodeIds.has(id)));
|
|
71
|
+
const state = /* @__PURE__ */ new Map();
|
|
72
|
+
const path = [];
|
|
73
|
+
const pathIndex = /* @__PURE__ */ new Map();
|
|
74
|
+
const cycles = [];
|
|
75
|
+
const cycleKeys = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (state.has(node.id)) continue;
|
|
78
|
+
visitForCycles(node.id, {
|
|
79
|
+
adjacency,
|
|
80
|
+
cycleKeys,
|
|
81
|
+
cycles,
|
|
82
|
+
path,
|
|
83
|
+
pathIndex,
|
|
84
|
+
state
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return cycles;
|
|
88
|
+
}
|
|
89
|
+
function visitForCycles(startId, visitState) {
|
|
90
|
+
const frames = [{
|
|
91
|
+
index: 0,
|
|
92
|
+
nodeId: startId
|
|
93
|
+
}];
|
|
94
|
+
markVisiting(startId, visitState);
|
|
95
|
+
while (frames.length > 0) {
|
|
96
|
+
const frame = frames.at(-1);
|
|
97
|
+
if (!frame) return;
|
|
98
|
+
const dependentId = (visitState.adjacency.get(frame.nodeId) ?? [])[frame.index];
|
|
99
|
+
if (!dependentId) {
|
|
100
|
+
markDone(frame.nodeId, visitState);
|
|
101
|
+
frames.pop();
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
frame.index += 1;
|
|
105
|
+
const dependentState = visitState.state.get(dependentId);
|
|
106
|
+
if (dependentState === "visiting") {
|
|
107
|
+
recordCycle(dependentId, visitState);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (dependentState === "done") continue;
|
|
111
|
+
markVisiting(dependentId, visitState);
|
|
112
|
+
frames.push({
|
|
113
|
+
index: 0,
|
|
114
|
+
nodeId: dependentId
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function markVisiting(nodeId, visitState) {
|
|
119
|
+
visitState.state.set(nodeId, "visiting");
|
|
120
|
+
visitState.pathIndex.set(nodeId, visitState.path.length);
|
|
121
|
+
visitState.path.push(nodeId);
|
|
122
|
+
}
|
|
123
|
+
function markDone(nodeId, visitState) {
|
|
124
|
+
visitState.state.set(nodeId, "done");
|
|
125
|
+
visitState.pathIndex.delete(nodeId);
|
|
126
|
+
visitState.path.pop();
|
|
127
|
+
}
|
|
128
|
+
function recordCycle(nodeId, visitState) {
|
|
129
|
+
const startIndex = visitState.pathIndex.get(nodeId);
|
|
130
|
+
if (startIndex === void 0) return;
|
|
131
|
+
const cycle = visitState.path.slice(startIndex);
|
|
132
|
+
const key = [...cycle].sort().join("\0");
|
|
133
|
+
if (visitState.cycleKeys.has(key)) return;
|
|
134
|
+
visitState.cycleKeys.add(key);
|
|
135
|
+
visitState.cycles.push(cycle);
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { dependentsByNeed, findDependencyCycles, findNode, flattenNodes, hasReachableDependent };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { parseRunnerCommandPayload, resolveRunnerEventSinkAuthToken } from "../runner-command-contract.js";
|
|
2
1
|
import { PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, RUNNERS_CONFIG_PATH } from "../config/defaults.js";
|
|
3
2
|
import { loadPipelineConfig, parsePipelineConfigParts } from "../config/load.js";
|
|
4
3
|
import "../config.js";
|
|
5
|
-
import { compileScheduleArtifact, parseScheduleArtifact } from "../
|
|
6
|
-
import "../
|
|
4
|
+
import { compileScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
|
|
5
|
+
import { parseRunnerCommandPayload, resolveRunnerEventSinkAuthToken } from "../runner-command-contract.js";
|
|
7
6
|
import { prepareRunnerGitWorkspace } from "../run-state/git-refs.js";
|
|
8
7
|
import { createRunnerEventSink } from "../runner-event-sink.js";
|
|
9
8
|
import { initialNodeStateStore } from "../runtime/node-state-store.js";
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readRunnerTaskDescriptor } from "./task-descriptor.js";
|
|
2
|
-
import { RunnerCommandPayloadValidationError, parseRunnerCommandPayload, resolveRunnerEventSinkAuthToken } from "../runner-command-contract.js";
|
|
3
2
|
import { loadPipelineConfig } from "../config/load.js";
|
|
4
3
|
import "../config.js";
|
|
5
|
-
import { compileScheduleArtifact, parseScheduleArtifact } from "../
|
|
6
|
-
import "../
|
|
4
|
+
import { compileScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
|
|
5
|
+
import { RunnerCommandPayloadValidationError, parseRunnerCommandPayload, resolveRunnerEventSinkAuthToken } from "../runner-command-contract.js";
|
|
7
6
|
import { commitAndPushNodeRef, mergeDependencyRefs, prepareRunnerGitWorkspace } from "../run-state/git-refs.js";
|
|
8
7
|
import { createRunnerEventSink } from "../runner-event-sink.js";
|
|
9
8
|
import { findPlannedNode } from "../planned-node.js";
|
|
@@ -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
|
-
quick: "quick";
|
|
47
46
|
full: "full";
|
|
47
|
+
quick: "quick";
|
|
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
|
-
quick: "quick";
|
|
108
107
|
full: "full";
|
|
108
|
+
quick: "quick";
|
|
109
109
|
}>;
|
|
110
110
|
}, z.core.$strict>, z.ZodObject<{
|
|
111
111
|
argv: z.ZodArray<z.ZodString>;
|