stageflow 0.9.0 → 0.13.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/README.md +6 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +331 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +12 -7
- package/dist/agent/piAdapter.js +194 -44
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +52 -1
- package/dist/agent/port.js +1 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +11 -1
- package/dist/config/loadStage.d.ts +1 -0
- package/dist/config/loadStage.js +207 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +56 -7
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +17 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +51 -0
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +117 -3
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +31 -0
- package/dist/runtime/verifiedStageExecution.js +127 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +137 -4
- package/dist/server/operatorResults.js +2 -0
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +41 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-Yzh42vhf.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +5 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/references/catalog-mapping.md +88 -3
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
3
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function failure(stageId, field, message) {
|
|
8
|
+
return loadFailure([
|
|
9
|
+
{
|
|
10
|
+
code: field === "completion"
|
|
11
|
+
? "pipeline.invalid_completion"
|
|
12
|
+
: "pipeline.invalid_recovery",
|
|
13
|
+
message: `Stage "${stageId}" ${field}: ${message}`,
|
|
14
|
+
category: "pipeline",
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
function hasOnlyKeys(value, allowed) {
|
|
19
|
+
return Object.keys(value).find((key) => !allowed.includes(key));
|
|
20
|
+
}
|
|
21
|
+
function isPositiveInteger(value) {
|
|
22
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
23
|
+
}
|
|
24
|
+
function isGateKind(value) {
|
|
25
|
+
return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
|
|
26
|
+
}
|
|
27
|
+
function parsePathFields(value) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!Array.isArray(value) ||
|
|
31
|
+
value.length === 0 ||
|
|
32
|
+
!value.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const fields = value.map((item) => item.trim());
|
|
36
|
+
return new Set(fields).size === fields.length ? fields : undefined;
|
|
37
|
+
}
|
|
38
|
+
function parseCheck(raw, stageId, index) {
|
|
39
|
+
if (!isPlainObject(raw)) {
|
|
40
|
+
return failure(stageId, "completion", `checks[${index}] must be an object`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw.id !== "string" || raw.id.trim() === "") {
|
|
43
|
+
return failure(stageId, "completion", `checks[${index}].id must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof raw.type !== "string") {
|
|
46
|
+
return failure(stageId, "completion", `checks[${index}].type must be a string`);
|
|
47
|
+
}
|
|
48
|
+
const id = raw.id.trim();
|
|
49
|
+
if (raw.type === "command") {
|
|
50
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "run", "cwd", "timeout_ms"]);
|
|
51
|
+
if (unknown)
|
|
52
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
53
|
+
if (typeof raw.run !== "string" || raw.run.trim() === "") {
|
|
54
|
+
return failure(stageId, "completion", `checks[${index}] command check requires a non-empty run`);
|
|
55
|
+
}
|
|
56
|
+
if (raw.cwd !== undefined && (typeof raw.cwd !== "string" || raw.cwd.trim() === "")) {
|
|
57
|
+
return failure(stageId, "completion", `checks[${index}].cwd must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
if (raw.timeout_ms !== undefined && !isPositiveInteger(raw.timeout_ms)) {
|
|
60
|
+
return failure(stageId, "completion", `checks[${index}].timeout_ms must be a positive integer`);
|
|
61
|
+
}
|
|
62
|
+
return loadSuccess({
|
|
63
|
+
id,
|
|
64
|
+
type: "command",
|
|
65
|
+
run: raw.run.trim(),
|
|
66
|
+
...(typeof raw.cwd === "string" ? { cwd: raw.cwd.trim() } : {}),
|
|
67
|
+
...(typeof raw.timeout_ms === "number" ? { timeout_ms: raw.timeout_ms } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (raw.type === "artifact") {
|
|
71
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "path", "nonempty"]);
|
|
72
|
+
if (unknown)
|
|
73
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
74
|
+
if (typeof raw.path !== "string" || raw.path.trim() === "") {
|
|
75
|
+
return failure(stageId, "completion", `checks[${index}] artifact check requires a non-empty path`);
|
|
76
|
+
}
|
|
77
|
+
const artifactPath = raw.path.trim();
|
|
78
|
+
const normalizedPath = path.normalize(artifactPath);
|
|
79
|
+
if (path.isAbsolute(artifactPath) ||
|
|
80
|
+
normalizedPath === ".." ||
|
|
81
|
+
normalizedPath.startsWith(`..${path.sep}`)) {
|
|
82
|
+
return failure(stageId, "completion", `checks[${index}] artifact path must stay inside the attempt artifact directory`);
|
|
83
|
+
}
|
|
84
|
+
if (raw.nonempty !== undefined && typeof raw.nonempty !== "boolean") {
|
|
85
|
+
return failure(stageId, "completion", `checks[${index}].nonempty must be a boolean`);
|
|
86
|
+
}
|
|
87
|
+
return loadSuccess({
|
|
88
|
+
id,
|
|
89
|
+
type: "artifact",
|
|
90
|
+
path: artifactPath,
|
|
91
|
+
...(typeof raw.nonempty === "boolean" ? { nonempty: raw.nonempty } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (raw.type === "checklist") {
|
|
95
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "items"]);
|
|
96
|
+
if (unknown)
|
|
97
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
98
|
+
if (!Array.isArray(raw.items) ||
|
|
99
|
+
raw.items.length === 0 ||
|
|
100
|
+
!raw.items.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
101
|
+
return failure(stageId, "completion", `checks[${index}] checklist requires a non-empty items array of strings`);
|
|
102
|
+
}
|
|
103
|
+
const items = raw.items.map((item) => item.trim());
|
|
104
|
+
if (new Set(items).size !== items.length) {
|
|
105
|
+
return failure(stageId, "completion", `checks[${index}] checklist items must be unique`);
|
|
106
|
+
}
|
|
107
|
+
return loadSuccess({ id, type: "checklist", items });
|
|
108
|
+
}
|
|
109
|
+
if (raw.type === "payload_schema") {
|
|
110
|
+
const unknown = hasOnlyKeys(raw, ["id", "type"]);
|
|
111
|
+
if (unknown)
|
|
112
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
113
|
+
return loadSuccess({ id, type: "payload_schema" });
|
|
114
|
+
}
|
|
115
|
+
if (raw.type === "gate") {
|
|
116
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "kind"]);
|
|
117
|
+
if (unknown)
|
|
118
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
119
|
+
if (!isGateKind(raw.kind)) {
|
|
120
|
+
return failure(stageId, "completion", `checks[${index}].kind must be one of ${STAGE_GATE_KINDS.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
return loadSuccess({ id, type: "gate", kind: raw.kind });
|
|
123
|
+
}
|
|
124
|
+
if (raw.type === "checkout_changes") {
|
|
125
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "path_fields"]);
|
|
126
|
+
if (unknown)
|
|
127
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
128
|
+
const pathFields = parsePathFields(raw.path_fields);
|
|
129
|
+
if (raw.path_fields !== undefined && pathFields === undefined) {
|
|
130
|
+
return failure(stageId, "completion", `checks[${index}].path_fields must be a unique, non-empty array of strings`);
|
|
131
|
+
}
|
|
132
|
+
return loadSuccess({
|
|
133
|
+
id,
|
|
134
|
+
type: "checkout_changes",
|
|
135
|
+
...(pathFields !== undefined ? { path_fields: pathFields } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return failure(stageId, "completion", `checks[${index}].type "${raw.type}" is unsupported`);
|
|
139
|
+
}
|
|
140
|
+
function parseCompletion(raw, stageId) {
|
|
141
|
+
if (raw === undefined)
|
|
142
|
+
return loadSuccess(undefined);
|
|
143
|
+
if (!isPlainObject(raw))
|
|
144
|
+
return failure(stageId, "completion", "must be an object");
|
|
145
|
+
const unknown = hasOnlyKeys(raw, ["mode", "checks"]);
|
|
146
|
+
if (unknown)
|
|
147
|
+
return failure(stageId, "completion", `unknown key "${unknown}"`);
|
|
148
|
+
if (raw.mode !== undefined && raw.mode !== "all") {
|
|
149
|
+
return failure(stageId, "completion", 'mode must be "all"');
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(raw.checks) || raw.checks.length === 0) {
|
|
152
|
+
return failure(stageId, "completion", "checks must be a non-empty array");
|
|
153
|
+
}
|
|
154
|
+
const checks = [];
|
|
155
|
+
for (let index = 0; index < raw.checks.length; index++) {
|
|
156
|
+
const outcome = parseCheck(raw.checks[index], stageId, index);
|
|
157
|
+
if (!outcome.ok)
|
|
158
|
+
return outcome;
|
|
159
|
+
checks.push(outcome.value);
|
|
160
|
+
}
|
|
161
|
+
const duplicates = checks.find((check, index) => checks.findIndex((item) => item.id === check.id) !== index);
|
|
162
|
+
if (duplicates)
|
|
163
|
+
return failure(stageId, "completion", `check id "${duplicates.id}" is duplicated`);
|
|
164
|
+
return loadSuccess({ mode: "all", checks });
|
|
165
|
+
}
|
|
166
|
+
function parseRecovery(raw, stageId, hasCompletion) {
|
|
167
|
+
if (raw === undefined)
|
|
168
|
+
return loadSuccess(undefined);
|
|
169
|
+
if (!hasCompletion)
|
|
170
|
+
return failure(stageId, "recovery", "requires a completion contract");
|
|
171
|
+
if (!isPlainObject(raw))
|
|
172
|
+
return failure(stageId, "recovery", "must be an object");
|
|
173
|
+
const unknown = hasOnlyKeys(raw, ["mode", "max_attempts", "retry_safety", "include_failed_checks"]);
|
|
174
|
+
if (unknown)
|
|
175
|
+
return failure(stageId, "recovery", `unknown key "${unknown}"`);
|
|
176
|
+
if (raw.mode !== "repair" && raw.mode !== "manual") {
|
|
177
|
+
return failure(stageId, "recovery", 'mode must be "repair" or "manual"');
|
|
178
|
+
}
|
|
179
|
+
if (raw.retry_safety !== "idempotent" && raw.retry_safety !== "side_effecting") {
|
|
180
|
+
return failure(stageId, "recovery", 'retry_safety must be "idempotent" or "side_effecting"');
|
|
181
|
+
}
|
|
182
|
+
if (raw.include_failed_checks !== undefined && typeof raw.include_failed_checks !== "boolean") {
|
|
183
|
+
return failure(stageId, "recovery", "include_failed_checks must be a boolean");
|
|
184
|
+
}
|
|
185
|
+
if (raw.mode === "repair") {
|
|
186
|
+
if (!isPositiveInteger(raw.max_attempts)) {
|
|
187
|
+
return failure(stageId, "recovery", "repair requires max_attempts to be a positive integer");
|
|
188
|
+
}
|
|
189
|
+
if (raw.retry_safety !== "idempotent") {
|
|
190
|
+
return failure(stageId, "recovery", "automatic repair requires retry_safety: idempotent");
|
|
191
|
+
}
|
|
192
|
+
return loadSuccess({
|
|
193
|
+
mode: "repair",
|
|
194
|
+
max_attempts: raw.max_attempts,
|
|
195
|
+
retry_safety: "idempotent",
|
|
196
|
+
include_failed_checks: raw.include_failed_checks ?? true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return loadSuccess({
|
|
200
|
+
mode: "manual",
|
|
201
|
+
retry_safety: raw.retry_safety,
|
|
202
|
+
...(typeof raw.include_failed_checks === "boolean"
|
|
203
|
+
? { include_failed_checks: raw.include_failed_checks }
|
|
204
|
+
: {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
export function parseExecutionPolicy(raw, stageId) {
|
|
208
|
+
const completion = parseCompletion(raw.completion, stageId);
|
|
209
|
+
if (!completion.ok)
|
|
210
|
+
return completion;
|
|
211
|
+
const recovery = parseRecovery(raw.recovery, stageId, completion.value !== undefined);
|
|
212
|
+
if (!recovery.ok)
|
|
213
|
+
return recovery;
|
|
214
|
+
return loadSuccess({
|
|
215
|
+
...(completion.value !== undefined ? { completion: completion.value } : {}),
|
|
216
|
+
...(recovery.value !== undefined ? { recovery: recovery.value } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
2
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isGateKind(value) {
|
|
7
|
+
return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
function failure(label, message) {
|
|
10
|
+
return loadFailure([
|
|
11
|
+
{
|
|
12
|
+
code: "stage.invalid_pre_emit_checks",
|
|
13
|
+
message: `Invalid stage ${label}: pre_emit_checks ${message}`,
|
|
14
|
+
category: "stage",
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
function parsePreEmitCheck(raw, label, index) {
|
|
19
|
+
if (!isPlainObject(raw)) {
|
|
20
|
+
return failure(label, `[${index}] must be an object`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw.id !== "string" || raw.id.trim() === "") {
|
|
23
|
+
return failure(label, `[${index}].id must be a non-empty string`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof raw.type !== "string") {
|
|
26
|
+
return failure(label, `[${index}].type must be a string`);
|
|
27
|
+
}
|
|
28
|
+
const id = raw.id.trim();
|
|
29
|
+
if (raw.type === "gate") {
|
|
30
|
+
if (!isGateKind(raw.kind)) {
|
|
31
|
+
return failure(label, `[${index}].kind must be one of ${STAGE_GATE_KINDS.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
return loadSuccess({ id, type: "gate", kind: raw.kind });
|
|
34
|
+
}
|
|
35
|
+
if (raw.type === "artifact_declared") {
|
|
36
|
+
if (typeof raw.basename !== "string" || raw.basename.trim() === "") {
|
|
37
|
+
return failure(label, `[${index}].basename must be a non-empty string`);
|
|
38
|
+
}
|
|
39
|
+
return loadSuccess({
|
|
40
|
+
id,
|
|
41
|
+
type: "artifact_declared",
|
|
42
|
+
basename: raw.basename.trim(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return failure(label, `[${index}].type "${raw.type}" is unsupported`);
|
|
46
|
+
}
|
|
47
|
+
export function parsePreEmitChecks(raw, label) {
|
|
48
|
+
if (raw === undefined)
|
|
49
|
+
return loadSuccess(undefined);
|
|
50
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
51
|
+
return failure(label, "must be a non-empty array");
|
|
52
|
+
}
|
|
53
|
+
const checks = [];
|
|
54
|
+
for (let index = 0; index < raw.length; index++) {
|
|
55
|
+
const outcome = parsePreEmitCheck(raw[index], label, index);
|
|
56
|
+
if (!outcome.ok)
|
|
57
|
+
return outcome;
|
|
58
|
+
checks.push(outcome.value);
|
|
59
|
+
}
|
|
60
|
+
const duplicate = checks.find((check, index) => checks.findIndex((item) => item.id === check.id) !== index);
|
|
61
|
+
if (duplicate) {
|
|
62
|
+
return failure(label, `id "${duplicate.id}" is duplicated`);
|
|
63
|
+
}
|
|
64
|
+
return loadSuccess(checks);
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NeedTerminalState, PipelineNeedEdge, PipelineNeeds, PipelineStageRef, ResolvedPipelineStageNode } from "../types/pipeline.js";
|
|
2
|
+
export declare const NEED_TERMINAL_STATES: readonly NeedTerminalState[];
|
|
3
|
+
export declare function isNeedTerminalState(value: unknown): value is NeedTerminalState;
|
|
4
|
+
export declare function parsePipelineNeeds(raw: unknown, stageId: string): {
|
|
5
|
+
ok: true;
|
|
6
|
+
value: PipelineNeeds;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function toNeedEdges(needs: PipelineStageRef["needs"]): PipelineNeedEdge[];
|
|
12
|
+
export declare function predecessorEdges(node: Pick<ResolvedPipelineStageNode, "needs" | "needsEdges">): PipelineNeedEdge[];
|
|
13
|
+
export declare function hydrateResolvedNeeds(node: {
|
|
14
|
+
needs?: unknown;
|
|
15
|
+
needsEdges?: unknown;
|
|
16
|
+
}): {
|
|
17
|
+
needs: string | null;
|
|
18
|
+
needsEdges: PipelineNeedEdge[];
|
|
19
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export const NEED_TERMINAL_STATES = [
|
|
2
|
+
"succeeded",
|
|
3
|
+
"failed",
|
|
4
|
+
"skipped",
|
|
5
|
+
];
|
|
6
|
+
const NEED_TERMINAL_STATE_SET = new Set(NEED_TERMINAL_STATES);
|
|
7
|
+
export function isNeedTerminalState(value) {
|
|
8
|
+
return typeof value === "string" && NEED_TERMINAL_STATE_SET.has(value);
|
|
9
|
+
}
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function parseNeedOn(raw, stageId, parentId) {
|
|
14
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
message: `stage "${stageId}": needs "${parentId}": on must be a non-empty unique subset of succeeded, failed, skipped`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const on = [];
|
|
22
|
+
for (const item of raw) {
|
|
23
|
+
if (typeof item !== "string" || !NEED_TERMINAL_STATE_SET.has(item) || seen.has(item)) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
message: `stage "${stageId}": needs "${parentId}": on must be a non-empty unique subset of succeeded, failed, skipped`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
seen.add(item);
|
|
30
|
+
on.push(item);
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, value: on };
|
|
33
|
+
}
|
|
34
|
+
export function parsePipelineNeeds(raw, stageId) {
|
|
35
|
+
if (typeof raw === "string") {
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return { ok: false, message: `stage "${stageId}": needs must be a non-empty string` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, value: raw };
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(raw)) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
message: `stage "${stageId}": needs must be a non-empty string or an array of at least two items`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (raw.length < 2) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: `stage "${stageId}": needs array must contain at least two items`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const edges = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const item of raw) {
|
|
56
|
+
if (typeof item === "string") {
|
|
57
|
+
if (!item) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: `stage "${stageId}": needs item must be a stage id or { id, on }`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (seen.has(item)) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
message: `stage "${stageId}": needs contains duplicate id "${item}"`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
seen.add(item);
|
|
70
|
+
edges.push({ id: item, on: ["succeeded"] });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!isPlainObject(item)) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
message: `stage "${stageId}": needs item must be a stage id or { id, on }`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
for (const key of Object.keys(item)) {
|
|
80
|
+
if (key !== "id" && key !== "on") {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
message: `stage "${stageId}": needs item: unknown key "${key}"`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (typeof item.id !== "string" || !item.id) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
message: `stage "${stageId}": needs item must be a stage id or { id, on }`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (seen.has(item.id)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: `stage "${stageId}": needs contains duplicate id "${item.id}"`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const onResult = parseNeedOn(item.on, stageId, item.id);
|
|
100
|
+
if (!onResult.ok)
|
|
101
|
+
return onResult;
|
|
102
|
+
seen.add(item.id);
|
|
103
|
+
edges.push({ id: item.id, on: onResult.value });
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, value: edges };
|
|
106
|
+
}
|
|
107
|
+
export function toNeedEdges(needs) {
|
|
108
|
+
if (needs === undefined)
|
|
109
|
+
return [];
|
|
110
|
+
if (typeof needs === "string") {
|
|
111
|
+
return needs ? [{ id: needs, on: ["succeeded"] }] : [];
|
|
112
|
+
}
|
|
113
|
+
return needs.map((item) => typeof item === "string"
|
|
114
|
+
? { id: item, on: ["succeeded"] }
|
|
115
|
+
: { id: item.id, on: item.on && item.on.length > 0 ? item.on : ["succeeded"] });
|
|
116
|
+
}
|
|
117
|
+
export function predecessorEdges(node) {
|
|
118
|
+
if (node.needsEdges !== undefined)
|
|
119
|
+
return node.needsEdges;
|
|
120
|
+
if (typeof node.needs === "string" && node.needs) {
|
|
121
|
+
return [{ id: node.needs, on: ["succeeded"] }];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
export function hydrateResolvedNeeds(node) {
|
|
126
|
+
if (Array.isArray(node.needsEdges)) {
|
|
127
|
+
const edges = [];
|
|
128
|
+
for (const item of node.needsEdges) {
|
|
129
|
+
if (!isPlainObject(item) || typeof item.id !== "string" || !item.id)
|
|
130
|
+
continue;
|
|
131
|
+
const on = Array.isArray(item.on)
|
|
132
|
+
? item.on.filter((state) => typeof state === "string" && NEED_TERMINAL_STATE_SET.has(state))
|
|
133
|
+
: ["succeeded"];
|
|
134
|
+
edges.push({ id: item.id, on: on.length > 0 ? on : ["succeeded"] });
|
|
135
|
+
}
|
|
136
|
+
if (typeof node.needs === "string") {
|
|
137
|
+
return { needs: node.needs, needsEdges: edges };
|
|
138
|
+
}
|
|
139
|
+
if (node.needs === null) {
|
|
140
|
+
return { needs: null, needsEdges: edges };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
needs: edges.length === 1 ? edges[0].id : null,
|
|
144
|
+
needsEdges: edges,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof node.needs === "string" && node.needs) {
|
|
148
|
+
return { needs: node.needs, needsEdges: toNeedEdges(node.needs) };
|
|
149
|
+
}
|
|
150
|
+
return { needs: null, needsEdges: [] };
|
|
151
|
+
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
export const WIRING_KEYS = new Set([
|
|
1
|
+
export const WIRING_KEYS = new Set([
|
|
2
|
+
"id",
|
|
3
|
+
"needs",
|
|
4
|
+
"fork",
|
|
5
|
+
"uses",
|
|
6
|
+
"clonable",
|
|
7
|
+
"clone_cap",
|
|
8
|
+
"completion",
|
|
9
|
+
"recovery",
|
|
10
|
+
"feedback_loop",
|
|
11
|
+
"replay_safe",
|
|
12
|
+
]);
|
|
2
13
|
export const BODY_KEYS = new Set([
|
|
3
14
|
"system_prompt",
|
|
4
15
|
"model",
|
|
5
16
|
"payload_schema",
|
|
6
17
|
"gate_kinds",
|
|
18
|
+
"pre_emit_checks",
|
|
19
|
+
"clone_input_schema",
|
|
20
|
+
"clone_actions",
|
|
21
|
+
"timeout_ms",
|
|
7
22
|
"skill",
|
|
23
|
+
"mcp",
|
|
8
24
|
]);
|
|
9
25
|
export function isPipelineStageBodyKey(key) {
|
|
10
26
|
return BODY_KEYS.has(key);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
1
|
+
import type { CloneAction, CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
2
2
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
3
|
-
export
|
|
3
|
+
export type ResolveCloneEmitOptions = {
|
|
4
|
+
allowedActions?: CloneAction[];
|
|
5
|
+
successorCloneInputSchemas?: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveCloneEmitContext(dag: ResolvedPipelineDag, stageId: string, options?: ResolveCloneEmitOptions): CloneEmitContext | undefined;
|
|
4
8
|
export declare function resolveForkEmitContext(dag: ResolvedPipelineDag, stageId: string): ForkEmitContext | undefined;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
export function resolveCloneEmitContext(dag, stageId) {
|
|
1
|
+
export function resolveCloneEmitContext(dag, stageId, options) {
|
|
2
2
|
const childIds = dag.childrenOf[stageId] ?? [];
|
|
3
3
|
const clonableSuccessors = [];
|
|
4
4
|
for (const childId of childIds) {
|
|
5
5
|
const node = dag.nodes.find((n) => n.id === childId);
|
|
6
6
|
if (node?.clonable === true) {
|
|
7
|
+
const cloneInputSchema = options?.successorCloneInputSchemas?.[childId];
|
|
7
8
|
clonableSuccessors.push({
|
|
8
9
|
successorId: childId,
|
|
9
10
|
cloneCap: node.clone_cap,
|
|
11
|
+
...(cloneInputSchema !== undefined ? { cloneInputSchema } : {}),
|
|
10
12
|
});
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
if (clonableSuccessors.length === 0)
|
|
14
16
|
return undefined;
|
|
15
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
clonableSuccessors,
|
|
19
|
+
...(options?.allowedActions !== undefined
|
|
20
|
+
? { allowedActions: options.allowedActions }
|
|
21
|
+
: {}),
|
|
22
|
+
};
|
|
16
23
|
}
|
|
17
24
|
export function resolveForkEmitContext(dag, stageId) {
|
|
18
25
|
const dagNode = dag.nodes.find((n) => n.id === stageId);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { PipelineStageRef, PipelineStageYamlEntry, ResolvedPipelineDag } from "../types/pipeline.js";
|
|
1
|
+
import type { PipelineStageRef, PipelineStageYamlEntry, ResolvedPipelineDag, FeedbackLoopConfig } from "../types/pipeline.js";
|
|
2
2
|
export type ResolvePipelineDagContext = {
|
|
3
3
|
pipelineId: string;
|
|
4
4
|
path: string;
|
|
5
5
|
};
|
|
6
|
+
export declare function parseFeedbackLoopConfig(value: unknown, stageId: string, ctx: ResolvePipelineDagContext): FeedbackLoopConfig;
|
|
6
7
|
export declare function parsePipelineStageEntries(raw: unknown, ctx: ResolvePipelineDagContext): PipelineStageYamlEntry[];
|
|
7
8
|
export declare function resolvePipelineDagFromRefs(refs: PipelineStageRef[], ctx: ResolvePipelineDagContext): {
|
|
8
9
|
stages: string[];
|