stageflow 0.2.0 → 0.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/README.md +179 -14
- package/dist/agent/activity.d.ts +12 -1
- package/dist/agent/activity.js +37 -2
- package/dist/agent/activityObserver.d.ts +5 -1
- package/dist/agent/activityObserver.js +174 -11
- package/dist/agent/cursorProvider.d.ts +1 -10
- package/dist/agent/cursorProvider.js +18 -10
- package/dist/agent/fakeAgent.js +15 -7
- package/dist/agent/piAdapter.d.ts +18 -1
- package/dist/agent/piAdapter.js +115 -45
- package/dist/agent/port.d.ts +7 -0
- package/dist/agent/port.js +3 -0
- package/dist/agent/providerAuth.js +12 -7
- package/dist/catalog/displayCatalogPath.d.ts +12 -0
- package/dist/catalog/displayCatalogPath.js +28 -0
- package/dist/cli/artifactCommand.d.ts +10 -0
- package/dist/cli/artifactCommand.js +120 -0
- package/dist/cli/envelopeCommand.d.ts +32 -0
- package/dist/cli/envelopeCommand.js +285 -0
- package/dist/cli/exportRunCommand.d.ts +10 -0
- package/dist/cli/exportRunCommand.js +150 -0
- package/dist/cli/handoffFormat.d.ts +22 -0
- package/dist/cli/handoffFormat.js +61 -0
- package/dist/cli/initCommand.d.ts +9 -0
- package/dist/cli/initCommand.js +71 -0
- package/dist/cli/initTemplates.d.ts +3 -0
- package/dist/cli/initTemplates.js +19 -0
- package/dist/cli/operatorCatalog.d.ts +10 -0
- package/dist/cli/operatorCatalog.js +16 -0
- package/dist/cli/runCommand.d.ts +7 -1
- package/dist/cli/runCommand.js +83 -11
- package/dist/cli/runOutput.d.ts +4 -1
- package/dist/cli/runOutput.js +36 -8
- package/dist/cli/skillsCommand.d.ts +16 -0
- package/dist/cli/skillsCommand.js +459 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +20 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -22
- package/dist/config/browseCatalog.d.ts +45 -0
- package/dist/config/browseCatalog.js +170 -0
- package/dist/config/createPipeline.d.ts +12 -3
- package/dist/config/createPipeline.js +292 -100
- package/dist/config/createStage.d.ts +10 -4
- package/dist/config/createStage.js +44 -14
- package/dist/config/listConfig.d.ts +14 -19
- package/dist/config/listConfig.js +26 -191
- package/dist/config/listModelsFromManifest.d.ts +2 -0
- package/dist/config/listModelsFromManifest.js +4 -0
- package/dist/config/loadOutcome.d.ts +4 -0
- package/dist/config/loadPipeline.d.ts +1 -15
- package/dist/config/loadPipeline.js +88 -142
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +60 -32
- package/dist/config/loadStageflowManifest.d.ts +5 -0
- package/dist/config/loadStageflowManifest.js +157 -0
- package/dist/config/mergePipelineIncludes.d.ts +9 -0
- package/dist/config/mergePipelineIncludes.js +141 -0
- package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
- package/dist/config/normalizePipelineStageEntry.js +210 -0
- package/dist/config/pipelineStageKeys.d.ts +4 -0
- package/dist/config/pipelineStageKeys.js +14 -0
- package/dist/config/resolveCatalogContext.d.ts +13 -0
- package/dist/config/resolveCatalogContext.js +13 -0
- package/dist/config/resolveForkEmitContext.d.ts +4 -0
- package/dist/config/resolveForkEmitContext.js +34 -0
- package/dist/config/resolvePipelineDag.d.ts +5 -1
- package/dist/config/resolvePipelineDag.js +86 -36
- package/dist/config/scanCatalogPaths.d.ts +4 -0
- package/dist/config/scanCatalogPaths.js +83 -0
- package/dist/config/validateCatalog.d.ts +22 -4
- package/dist/config/validateCatalog.js +169 -132
- package/dist/envelope/check.d.ts +2 -0
- package/dist/envelope/check.js +58 -0
- package/dist/envelope/cloneForks.d.ts +2 -0
- package/dist/envelope/cloneForks.js +62 -0
- package/dist/envelope/forkChoice.d.ts +3 -0
- package/dist/envelope/forkChoice.js +47 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mcp/catalogTools.d.ts +3 -0
- package/dist/mcp/catalogTools.js +187 -0
- package/dist/mcp/controlTools.d.ts +3 -0
- package/dist/mcp/controlTools.js +147 -0
- package/dist/mcp/deps.d.ts +11 -0
- package/dist/mcp/deps.js +1 -0
- package/dist/mcp/projectRun.d.ts +1 -37
- package/dist/mcp/projectRun.js +1 -57
- package/dist/mcp/resources.d.ts +5 -0
- package/dist/mcp/resources.js +45 -0
- package/dist/mcp/server.d.ts +16 -3
- package/dist/mcp/server.js +159 -3
- package/dist/mcp/toolResults.d.ts +7 -0
- package/dist/mcp/toolResults.js +6 -0
- package/dist/mcp/tools.d.ts +2 -7
- package/dist/mcp/tools.js +52 -86
- package/dist/mcp/waitRun.d.ts +46 -0
- package/dist/mcp/waitRun.js +146 -0
- package/dist/mcp/waitingGates.d.ts +12 -0
- package/dist/mcp/waitingGates.js +83 -0
- package/dist/project/findProjectRoot.d.ts +2 -0
- package/dist/project/findProjectRoot.js +51 -0
- package/dist/project/globalHome.d.ts +2 -0
- package/dist/project/globalHome.js +19 -0
- package/dist/project/resolveProjectContext.d.ts +9 -0
- package/dist/project/resolveProjectContext.js +38 -0
- package/dist/project/resolveStageflowContext.d.ts +11 -0
- package/dist/project/resolveStageflowContext.js +66 -0
- package/dist/projection/projectRun.d.ts +44 -0
- package/dist/projection/projectRun.js +71 -0
- package/dist/prompt/priorEnvelope.d.ts +1 -1
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +4 -0
- package/dist/runstore/paths.js +2 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
- package/dist/runstore/pipelineDagSnapshot.js +90 -1
- package/dist/runstore/port.d.ts +22 -2
- package/dist/runstore/port.js +4 -1
- package/dist/runstore/runProjection.d.ts +1 -1
- package/dist/runstore/runProjection.js +25 -4
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
- package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +4 -1
- package/dist/runstore/stageInstanceId.d.ts +4 -0
- package/dist/runstore/stageInstanceId.js +23 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
- package/dist/runstore/syntheticStageSnapshot.js +2 -1
- package/dist/runstore/trackProjection.js +28 -12
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +1 -1
- package/dist/runtime/cloneSchedule.d.ts +18 -0
- package/dist/runtime/cloneSchedule.js +233 -0
- package/dist/runtime/credentialBinding.d.ts +8 -6
- package/dist/runtime/credentialBinding.js +18 -24
- package/dist/runtime/envelopeRouting.d.ts +2 -1
- package/dist/runtime/envelopeRouting.js +80 -13
- package/dist/runtime/pipelineRunner.d.ts +3 -0
- package/dist/runtime/pipelineRunner.js +15 -1
- package/dist/runtime/pipelineScheduler.d.ts +8 -2
- package/dist/runtime/pipelineScheduler.js +215 -39
- package/dist/runtime/reloadRunCatalog.d.ts +5 -0
- package/dist/runtime/reloadRunCatalog.js +44 -0
- package/dist/runtime/resumeReconstruct.d.ts +1 -0
- package/dist/runtime/resumeReconstruct.js +31 -10
- package/dist/runtime/runChangeBus.d.ts +18 -0
- package/dist/runtime/runChangeBus.js +83 -0
- package/dist/runtime/runManager.d.ts +4 -0
- package/dist/runtime/runManager.js +36 -16
- package/dist/runtime/settingsFile.d.ts +12 -0
- package/dist/runtime/settingsFile.js +87 -6
- package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
- package/dist/runtime/stageAttemptBootstrap.js +18 -7
- package/dist/runtime/stageProcessLauncher.js +3 -0
- package/dist/runtime/stageRecovery.js +3 -1
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +17 -15
- package/dist/runtime/stageWorker.js +13 -10
- package/dist/server/bootstrap.d.ts +32 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/createHttpHost.d.ts +34 -0
- package/dist/server/createHttpHost.js +70 -0
- package/dist/server/http.d.ts +6 -13
- package/dist/server/http.js +297 -352
- package/dist/server/mcpHost.d.ts +22 -0
- package/dist/server/mcpHost.js +21 -0
- package/dist/server/operatorResults.d.ts +13 -0
- package/dist/server/operatorResults.js +22 -0
- package/dist/tools/emitStageEnvelope.d.ts +12 -1
- package/dist/tools/emitStageEnvelope.js +27 -1
- package/dist/types/envelope.d.ts +3 -0
- package/dist/types/forkChoice.d.ts +30 -0
- package/dist/types/forkChoice.js +1 -0
- package/dist/types/pipeline.d.ts +59 -2
- package/dist/types/stageflowManifest.d.ts +23 -0
- package/dist/types/stageflowManifest.js +1 -0
- package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +118 -0
- package/dist/ui/index.html +3 -2
- package/dist/ui/stageflow-icon.svg +12 -0
- package/package.json +8 -5
- package/skills/install-suite.sh +107 -0
- package/skills/stageflow/SKILL.md +26 -0
- package/skills/stageflow/references/control-surface.md +26 -0
- package/skills/stageflow/scripts/detect-host.mjs +54 -0
- package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
- package/skills/stageflow-author/SKILL.md +58 -0
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
- package/skills/stageflow-author/references/catalog-mapping.md +131 -0
- package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
- package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
- package/skills/stageflow-author/references/validate-and-report.md +38 -0
- package/skills/stageflow-delegate/SKILL.md +18 -0
- package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
- package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
- package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
- package/skills/stageflow-run/SKILL.md +151 -0
- package/skills/stageflow-run/references/mcp-call.md +17 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
- package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
- package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
- package/skills/stageflow-session-capture/SKILL.md +94 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
- package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
- package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
- package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
- package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
- package/skills/stageflow-setup/SKILL.md +110 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
package/dist/config/loadStage.js
CHANGED
|
@@ -5,14 +5,14 @@ import { readYamlObject } from "./readYamlObject.js";
|
|
|
5
5
|
function isGateKind(value) {
|
|
6
6
|
return STAGE_GATE_KINDS.includes(value);
|
|
7
7
|
}
|
|
8
|
-
function parseGateKinds(raw,
|
|
8
|
+
function parseGateKinds(raw, label) {
|
|
9
9
|
if (raw === undefined)
|
|
10
10
|
return loadSuccess(undefined);
|
|
11
11
|
if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
|
|
12
12
|
return loadFailure([
|
|
13
13
|
{
|
|
14
14
|
code: "stage.invalid_gate_kinds",
|
|
15
|
-
message: `Invalid stage
|
|
15
|
+
message: `Invalid stage ${label}: gate_kinds must be an array of strings`,
|
|
16
16
|
category: "stage",
|
|
17
17
|
},
|
|
18
18
|
]);
|
|
@@ -23,7 +23,7 @@ function parseGateKinds(raw, filePath) {
|
|
|
23
23
|
return loadFailure([
|
|
24
24
|
{
|
|
25
25
|
code: "stage.invalid_gate_kinds",
|
|
26
|
-
message: `Invalid stage
|
|
26
|
+
message: `Invalid stage ${label}: unsupported gate kind "${item}" (allowed: ${STAGE_GATE_KINDS.join(", ")})`,
|
|
27
27
|
category: "stage",
|
|
28
28
|
},
|
|
29
29
|
]);
|
|
@@ -32,35 +32,20 @@ function parseGateKinds(raw, filePath) {
|
|
|
32
32
|
}
|
|
33
33
|
return loadSuccess(kinds.length > 0 ? kinds : undefined);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
raw = await readYamlObject(filePath);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
-
return loadFailure([
|
|
43
|
-
{
|
|
44
|
-
code: "stage.load_error",
|
|
45
|
-
message,
|
|
46
|
-
category: "stage",
|
|
47
|
-
},
|
|
48
|
-
]);
|
|
49
|
-
}
|
|
50
|
-
if (typeof raw?.id !== "string" ||
|
|
51
|
-
typeof raw?.system_prompt !== "string" ||
|
|
52
|
-
typeof raw?.model !== "string") {
|
|
35
|
+
function parseStageFields(raw, label, entryId) {
|
|
36
|
+
if (typeof raw.system_prompt !== "string" ||
|
|
37
|
+
typeof raw.model !== "string") {
|
|
53
38
|
return loadFailure([
|
|
54
39
|
{
|
|
55
40
|
code: "stage.invalid_shape",
|
|
56
|
-
message: `Invalid stage
|
|
41
|
+
message: `Invalid stage ${label}: system_prompt and model are required strings`,
|
|
57
42
|
category: "stage",
|
|
58
|
-
stageId:
|
|
43
|
+
stageId: entryId,
|
|
59
44
|
},
|
|
60
45
|
]);
|
|
61
46
|
}
|
|
62
47
|
const stage = {
|
|
63
|
-
id:
|
|
48
|
+
id: entryId,
|
|
64
49
|
system_prompt: raw.system_prompt,
|
|
65
50
|
model: raw.model,
|
|
66
51
|
};
|
|
@@ -71,9 +56,9 @@ export async function loadStageOutcome(filePath) {
|
|
|
71
56
|
return loadFailure([
|
|
72
57
|
{
|
|
73
58
|
code: "stage.invalid_payload_schema",
|
|
74
|
-
message: `Invalid stage
|
|
59
|
+
message: `Invalid stage ${label}: payload_schema must be an object`,
|
|
75
60
|
category: "stage",
|
|
76
|
-
stageId:
|
|
61
|
+
stageId: entryId,
|
|
77
62
|
},
|
|
78
63
|
]);
|
|
79
64
|
}
|
|
@@ -85,19 +70,19 @@ export async function loadStageOutcome(filePath) {
|
|
|
85
70
|
return loadFailure([
|
|
86
71
|
{
|
|
87
72
|
code: "stage.invalid_payload_schema",
|
|
88
|
-
message: `Invalid stage
|
|
73
|
+
message: `Invalid stage ${label}: invalid payload_schema: ${message}`,
|
|
89
74
|
category: "stage",
|
|
90
|
-
stageId:
|
|
75
|
+
stageId: entryId,
|
|
91
76
|
},
|
|
92
77
|
]);
|
|
93
78
|
}
|
|
94
79
|
stage.payload_schema = raw.payload_schema;
|
|
95
80
|
}
|
|
96
|
-
const gateKindsOutcome = parseGateKinds(raw.gate_kinds,
|
|
81
|
+
const gateKindsOutcome = parseGateKinds(raw.gate_kinds, label);
|
|
97
82
|
if (!gateKindsOutcome.ok) {
|
|
98
83
|
const issues = gateKindsOutcome.issues.map((issue) => ({
|
|
99
84
|
...issue,
|
|
100
|
-
stageId:
|
|
85
|
+
stageId: entryId,
|
|
101
86
|
}));
|
|
102
87
|
return loadFailure(issues);
|
|
103
88
|
}
|
|
@@ -108,9 +93,9 @@ export async function loadStageOutcome(filePath) {
|
|
|
108
93
|
return loadFailure([
|
|
109
94
|
{
|
|
110
95
|
code: "stage.invalid_skill",
|
|
111
|
-
message: `Invalid stage
|
|
96
|
+
message: `Invalid stage ${label}: skill must be a non-empty string`,
|
|
112
97
|
category: "stage",
|
|
113
|
-
stageId:
|
|
98
|
+
stageId: entryId,
|
|
114
99
|
},
|
|
115
100
|
]);
|
|
116
101
|
}
|
|
@@ -118,6 +103,49 @@ export async function loadStageOutcome(filePath) {
|
|
|
118
103
|
}
|
|
119
104
|
return loadSuccess(stage);
|
|
120
105
|
}
|
|
106
|
+
export function loadStageFromObjectOutcome(raw, ctx) {
|
|
107
|
+
const label = `${ctx.entryId} (${ctx.declaringPath})`;
|
|
108
|
+
return parseStageFields(raw, label, ctx.entryId);
|
|
109
|
+
}
|
|
110
|
+
export async function loadStageOutcome(filePath) {
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = await readYamlObject(filePath);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
return loadFailure([
|
|
118
|
+
{
|
|
119
|
+
code: "stage.load_error",
|
|
120
|
+
message,
|
|
121
|
+
category: "stage",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
if (typeof raw?.id !== "string") {
|
|
126
|
+
return loadFailure([
|
|
127
|
+
{
|
|
128
|
+
code: "stage.invalid_shape",
|
|
129
|
+
message: `Invalid stage file ${filePath}: id, system_prompt, and model are required strings`,
|
|
130
|
+
category: "stage",
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
const outcome = parseStageFields(raw, `file ${filePath}`, raw.id);
|
|
135
|
+
if (!outcome.ok)
|
|
136
|
+
return outcome;
|
|
137
|
+
if (outcome.value.id !== raw.id) {
|
|
138
|
+
return loadFailure([
|
|
139
|
+
{
|
|
140
|
+
code: "stage.invalid_shape",
|
|
141
|
+
message: `Invalid stage file ${filePath}: id mismatch`,
|
|
142
|
+
category: "stage",
|
|
143
|
+
stageId: raw.id,
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
return outcome;
|
|
148
|
+
}
|
|
121
149
|
export async function loadStage(filePath) {
|
|
122
150
|
const outcome = await loadStageOutcome(filePath);
|
|
123
151
|
if (!outcome.ok) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoadedManifest } from "../types/stageflowManifest.js";
|
|
2
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
export declare function manifestPathForProject(projectRoot: string): string;
|
|
4
|
+
export declare function parseStageflowManifestOutcome(yamlText: string, manifestPath: string, projectRoot: string): LoadOutcome<LoadedManifest>;
|
|
5
|
+
export declare function loadStageflowManifestOutcome(projectRoot: string): Promise<LoadOutcome<LoadedManifest>>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
5
|
+
const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
|
|
6
|
+
const DEFAULT_TASK_PATTERN = "*.task.yaml";
|
|
7
|
+
export function manifestPathForProject(projectRoot) {
|
|
8
|
+
return path.join(projectRoot, "stageflow.yaml");
|
|
9
|
+
}
|
|
10
|
+
function catalogIssue(code, message) {
|
|
11
|
+
return { code, message, category: "catalog" };
|
|
12
|
+
}
|
|
13
|
+
function normalizeEntryPath(entry) {
|
|
14
|
+
return entry.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
15
|
+
}
|
|
16
|
+
function parseStringArray(raw, field, issues) {
|
|
17
|
+
if (!Array.isArray(raw)) {
|
|
18
|
+
issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} must be an array`));
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const entries = [];
|
|
22
|
+
for (const item of raw) {
|
|
23
|
+
if (typeof item !== "string" || item.trim().length === 0) {
|
|
24
|
+
issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} entries must be non-empty strings`));
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const normalized = normalizeEntryPath(item.trim());
|
|
28
|
+
if (path.isAbsolute(normalized)) {
|
|
29
|
+
issues.push(catalogIssue("catalog.manifest_invalid", `catalog.${field} entries must be repo-relative paths`));
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
entries.push(normalized);
|
|
33
|
+
}
|
|
34
|
+
return entries;
|
|
35
|
+
}
|
|
36
|
+
function parsePatterns(raw, issues) {
|
|
37
|
+
const defaults = {
|
|
38
|
+
pipeline: DEFAULT_PIPELINE_PATTERN,
|
|
39
|
+
task: DEFAULT_TASK_PATTERN,
|
|
40
|
+
};
|
|
41
|
+
if (raw === undefined || raw === null) {
|
|
42
|
+
return defaults;
|
|
43
|
+
}
|
|
44
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
45
|
+
issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns must be an object"));
|
|
46
|
+
return defaults;
|
|
47
|
+
}
|
|
48
|
+
const record = raw;
|
|
49
|
+
const pipeline = record.pipeline === undefined
|
|
50
|
+
? defaults.pipeline
|
|
51
|
+
: typeof record.pipeline === "string" && record.pipeline.trim().length > 0
|
|
52
|
+
? record.pipeline.trim()
|
|
53
|
+
: null;
|
|
54
|
+
const task = record.task === undefined
|
|
55
|
+
? defaults.task
|
|
56
|
+
: typeof record.task === "string" && record.task.trim().length > 0
|
|
57
|
+
? record.task.trim()
|
|
58
|
+
: null;
|
|
59
|
+
if (pipeline === null) {
|
|
60
|
+
issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns.pipeline must be a string"));
|
|
61
|
+
}
|
|
62
|
+
if (task === null) {
|
|
63
|
+
issues.push(catalogIssue("catalog.manifest_invalid", "catalog.patterns.task must be a string"));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
pipeline: pipeline ?? defaults.pipeline,
|
|
67
|
+
task: task ?? defaults.task,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoot) {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = parseYaml(yamlText);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return loadFailure([catalogIssue("catalog.manifest_load_error", message)]);
|
|
78
|
+
}
|
|
79
|
+
const issues = [];
|
|
80
|
+
const record = raw;
|
|
81
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
82
|
+
return loadFailure([
|
|
83
|
+
catalogIssue("catalog.manifest_invalid", "stageflow.yaml must be a mapping"),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
if (record.version !== 1) {
|
|
87
|
+
issues.push(catalogIssue("catalog.manifest_invalid", `Unsupported manifest version: ${record.version}`));
|
|
88
|
+
}
|
|
89
|
+
const catalogRaw = record.catalog;
|
|
90
|
+
if (catalogRaw === undefined ||
|
|
91
|
+
typeof catalogRaw !== "object" ||
|
|
92
|
+
catalogRaw === null ||
|
|
93
|
+
Array.isArray(catalogRaw)) {
|
|
94
|
+
issues.push(catalogIssue("catalog.manifest_invalid", "catalog object is required"));
|
|
95
|
+
if (issues.length > 0) {
|
|
96
|
+
return loadFailure(issues);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const catalogRecord = catalogRaw;
|
|
100
|
+
const pipelines = parseStringArray(catalogRecord.pipelines, "pipelines", issues);
|
|
101
|
+
const tasks = parseStringArray(catalogRecord.tasks, "tasks", issues);
|
|
102
|
+
const excludeRaw = catalogRecord.exclude;
|
|
103
|
+
let exclude;
|
|
104
|
+
if (excludeRaw !== undefined) {
|
|
105
|
+
const parsed = parseStringArray(excludeRaw, "exclude", issues);
|
|
106
|
+
if (parsed !== null) {
|
|
107
|
+
exclude = parsed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const patterns = parsePatterns(catalogRecord.patterns, issues);
|
|
111
|
+
if (issues.length > 0) {
|
|
112
|
+
return loadFailure(issues);
|
|
113
|
+
}
|
|
114
|
+
if (pipelines === null || tasks === null) {
|
|
115
|
+
return loadFailure(issues);
|
|
116
|
+
}
|
|
117
|
+
const catalog = { pipelines, tasks };
|
|
118
|
+
if (exclude !== undefined) {
|
|
119
|
+
catalog.exclude = exclude;
|
|
120
|
+
}
|
|
121
|
+
if (catalogRecord.patterns !== undefined) {
|
|
122
|
+
catalog.patterns = {};
|
|
123
|
+
if (typeof catalogRecord.patterns.pipeline === "string") {
|
|
124
|
+
catalog.patterns.pipeline = patterns.pipeline;
|
|
125
|
+
}
|
|
126
|
+
if (typeof catalogRecord.patterns.task === "string") {
|
|
127
|
+
catalog.patterns.task = patterns.task;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const manifest = { version: 1, catalog };
|
|
131
|
+
return loadSuccess({
|
|
132
|
+
path: manifestPath,
|
|
133
|
+
projectRoot,
|
|
134
|
+
manifest,
|
|
135
|
+
patterns,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export async function loadStageflowManifestOutcome(projectRoot) {
|
|
139
|
+
const manifestPath = manifestPathForProject(projectRoot);
|
|
140
|
+
try {
|
|
141
|
+
await access(manifestPath);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return loadFailure([
|
|
145
|
+
catalogIssue("catalog.manifest_load_error", `Manifest not found: ${manifestPath}`),
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
let yamlText;
|
|
149
|
+
try {
|
|
150
|
+
yamlText = await readFile(manifestPath, "utf8");
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
return loadFailure([catalogIssue("catalog.manifest_load_error", message)]);
|
|
155
|
+
}
|
|
156
|
+
return parseStageflowManifestOutcome(yamlText, manifestPath, projectRoot);
|
|
157
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
2
|
+
export type RawMergedEntry = {
|
|
3
|
+
raw: unknown;
|
|
4
|
+
declaringPath: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function mergePipelineStages(rootPath: string): Promise<LoadOutcome<{
|
|
7
|
+
entries: RawMergedEntry[];
|
|
8
|
+
pipelineId: string;
|
|
9
|
+
}>>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
|
+
import { readYamlObject } from "./readYamlObject.js";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function normalizePath(filePath) {
|
|
8
|
+
return path.normalize(path.resolve(filePath));
|
|
9
|
+
}
|
|
10
|
+
function formatIncludeCycle(stack, nextPath) {
|
|
11
|
+
const chain = [...stack, nextPath].map((p) => path.basename(p));
|
|
12
|
+
return chain.join(" → ");
|
|
13
|
+
}
|
|
14
|
+
async function visitPipelineFile(filePath, stack, idLocations, entries) {
|
|
15
|
+
const absPath = normalizePath(filePath);
|
|
16
|
+
if (stack.some((p) => normalizePath(p) === absPath)) {
|
|
17
|
+
const cycleChain = formatIncludeCycle(stack, absPath);
|
|
18
|
+
return loadFailure([
|
|
19
|
+
{
|
|
20
|
+
code: "pipeline.include_cycle",
|
|
21
|
+
message: `Include cycle detected: ${cycleChain}`,
|
|
22
|
+
category: "pipeline",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readYamlObject(absPath);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return loadFailure([
|
|
33
|
+
{
|
|
34
|
+
code: "pipeline.load_error",
|
|
35
|
+
message,
|
|
36
|
+
category: "pipeline",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
const nextStack = [...stack, absPath];
|
|
41
|
+
if (raw.include !== undefined) {
|
|
42
|
+
if (!Array.isArray(raw.include)) {
|
|
43
|
+
return loadFailure([
|
|
44
|
+
{
|
|
45
|
+
code: "pipeline.include_invalid",
|
|
46
|
+
message: `Invalid include in ${absPath}: include must be an array`,
|
|
47
|
+
category: "pipeline",
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
for (let index = 0; index < raw.include.length; index++) {
|
|
52
|
+
const item = raw.include[index];
|
|
53
|
+
if (!isPlainObject(item) || typeof item.local !== "string" || !item.local) {
|
|
54
|
+
return loadFailure([
|
|
55
|
+
{
|
|
56
|
+
code: "pipeline.include_invalid",
|
|
57
|
+
message: `Invalid include entry at index ${index} in ${absPath}: expected { local: "<path>" }`,
|
|
58
|
+
category: "pipeline",
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
const includePath = normalizePath(path.resolve(path.dirname(absPath), item.local));
|
|
63
|
+
const includeResult = await visitPipelineFile(includePath, nextStack, idLocations, entries);
|
|
64
|
+
if (!includeResult.ok)
|
|
65
|
+
return includeResult;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(raw.stages)) {
|
|
69
|
+
for (const entry of raw.stages) {
|
|
70
|
+
if (isPlainObject(entry)) {
|
|
71
|
+
const entryId = typeof entry.id === "string" ? entry.id : undefined;
|
|
72
|
+
if (entryId) {
|
|
73
|
+
const priorPath = idLocations.get(entryId);
|
|
74
|
+
if (priorPath) {
|
|
75
|
+
return loadFailure([
|
|
76
|
+
{
|
|
77
|
+
code: "pipeline.include_duplicate_stage",
|
|
78
|
+
message: `Duplicate stage id "${entryId}" in ${absPath} (also declared in ${priorPath})`,
|
|
79
|
+
category: "pipeline",
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
idLocations.set(entryId, absPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
entries.push({ raw: entry, declaringPath: absPath });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return loadSuccess(undefined);
|
|
90
|
+
}
|
|
91
|
+
export async function mergePipelineStages(rootPath) {
|
|
92
|
+
const absRoot = normalizePath(rootPath);
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
raw = await readYamlObject(absRoot);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return loadFailure([
|
|
100
|
+
{
|
|
101
|
+
code: "pipeline.load_error",
|
|
102
|
+
message,
|
|
103
|
+
category: "pipeline",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
if (typeof raw.id !== "string" || !raw.id) {
|
|
108
|
+
return loadFailure([
|
|
109
|
+
{
|
|
110
|
+
code: "pipeline.invalid_shape",
|
|
111
|
+
message: `Invalid pipeline ${absRoot}: id is required`,
|
|
112
|
+
category: "pipeline",
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(raw.stages)) {
|
|
117
|
+
return loadFailure([
|
|
118
|
+
{
|
|
119
|
+
code: "pipeline.invalid_shape",
|
|
120
|
+
message: `Invalid pipeline ${absRoot}: stages[] is required`,
|
|
121
|
+
category: "pipeline",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
const pipelineId = raw.id;
|
|
126
|
+
const idLocations = new Map();
|
|
127
|
+
const entries = [];
|
|
128
|
+
const mergeResult = await visitPipelineFile(absRoot, [], idLocations, entries);
|
|
129
|
+
if (!mergeResult.ok)
|
|
130
|
+
return mergeResult;
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
return loadFailure([
|
|
133
|
+
{
|
|
134
|
+
code: "pipeline.invalid_shape",
|
|
135
|
+
message: `Invalid pipeline ${absRoot}: stages must be non-empty`,
|
|
136
|
+
category: "pipeline",
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
return loadSuccess({ entries, pipelineId });
|
|
141
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NormalizedPipelineStageEntry } from "../types/pipeline.js";
|
|
2
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
import type { RawMergedEntry } from "./mergePipelineIncludes.js";
|
|
4
|
+
export declare function inferIdFromUsesPath(usesPath: string): string | null;
|
|
5
|
+
export declare function normalizePipelineStageEntries(rawEntries: RawMergedEntry[], ctx: {
|
|
6
|
+
pipelineId: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}): LoadOutcome<NormalizedPipelineStageEntry[]>;
|
|
9
|
+
export declare function toWiringRefs(entries: NormalizedPipelineStageEntry[]): Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
needs?: string;
|
|
12
|
+
fork?: {
|
|
13
|
+
select: "one" | "subset";
|
|
14
|
+
allow_none?: boolean;
|
|
15
|
+
};
|
|
16
|
+
clonable?: boolean;
|
|
17
|
+
clone_cap?: number;
|
|
18
|
+
}>;
|