stageflow 0.1.0 → 0.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/README.md +190 -13
- package/dist/agent/activity.d.ts +10 -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/fakeAgent.js +4 -0
- package/dist/agent/piAdapter.d.ts +16 -0
- package/dist/agent/piAdapter.js +88 -21
- package/dist/agent/port.d.ts +2 -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/ciIdentity.d.ts +14 -0
- package/dist/cli/ciIdentity.js +52 -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 +26 -0
- package/dist/cli/runCommand.js +209 -0
- package/dist/cli/runOutput.d.ts +19 -0
- package/dist/cli/runOutput.js +125 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +20 -3
- package/dist/cli.d.ts +12 -1
- package/dist/cli.js +82 -66
- 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 +84 -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 +16 -0
- package/dist/config/normalizePipelineStageEntry.js +186 -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 +3 -0
- package/dist/config/resolveForkEmitContext.js +12 -0
- package/dist/config/resolvePipelineDag.d.ts +5 -1
- package/dist/config/resolvePipelineDag.js +54 -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.js +7 -0
- package/dist/envelope/forkChoice.d.ts +3 -0
- package/dist/envelope/forkChoice.js +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp/projectRun.d.ts +3 -0
- package/dist/mcp/projectRun.js +7 -0
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +28 -11
- 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/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +4 -0
- package/dist/runstore/port.d.ts +15 -0
- package/dist/runstore/runProjection.js +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +46 -4
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +7 -1
- package/dist/runtime/credentialBinding.d.ts +8 -6
- package/dist/runtime/credentialBinding.js +18 -24
- package/dist/runtime/pipelineRunner.d.ts +11 -0
- package/dist/runtime/pipelineRunner.js +25 -1
- package/dist/runtime/pipelineScheduler.d.ts +3 -0
- package/dist/runtime/pipelineScheduler.js +70 -14
- 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 +27 -10
- package/dist/runtime/runManager.d.ts +8 -0
- package/dist/runtime/runManager.js +49 -18
- package/dist/runtime/settingsFile.d.ts +12 -0
- package/dist/runtime/settingsFile.js +87 -6
- package/dist/runtime/stageAttemptBootstrap.js +3 -0
- package/dist/runtime/stageProcessLauncher.d.ts +1 -0
- package/dist/runtime/stageProcessLauncher.js +6 -0
- package/dist/runtime/stageRecovery.js +3 -1
- package/dist/runtime/stageRunner.d.ts +2 -0
- package/dist/runtime/stageRunner.js +9 -2
- package/dist/runtime/stageWorker.js +4 -6
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
- package/dist/server/http.js +33 -9
- package/dist/tools/emitStageEnvelope.d.ts +3 -1
- package/dist/tools/emitStageEnvelope.js +11 -1
- package/dist/types/envelope.d.ts +1 -0
- package/dist/types/forkChoice.d.ts +8 -0
- package/dist/types/forkChoice.js +1 -0
- package/dist/types/pipeline.d.ts +51 -2
- package/dist/types/stageflowManifest.d.ts +23 -0
- package/dist/types/stageflowManifest.js +1 -0
- package/dist/ui/assets/index-CFSzDZje.js +118 -0
- package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
- package/dist/ui/index.html +3 -2
- package/dist/ui/stageflow-icon.svg +12 -0
- package/package.json +7 -5
- package/dist/agent/cursorExtension.d.ts +0 -12
- package/dist/agent/cursorExtension.js +0 -88
- package/dist/runstore/catalog.d.ts +0 -8
- package/dist/runstore/catalog.js +0 -16
- package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
- package/dist/runstore/disk/DiskRunStore.js +0 -238
- package/dist/runstore/layout.d.ts +0 -22
- package/dist/runstore/layout.js +0 -58
- package/dist/runtime/hitlSeams.d.ts +0 -38
- package/dist/runtime/hitlSeams.js +0 -2
- package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
1
2
|
import type { LoadIssue } from "./loadOutcome.js";
|
|
2
3
|
export type ValidationSeverity = "error" | "warning";
|
|
3
|
-
export type ValidationCategory = "pipeline" | "stage" | "catalog";
|
|
4
|
-
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "stage.load_error" | "stage.id_filename_mismatch" | "catalog.duplicate_pipeline_id" | "catalog.
|
|
4
|
+
export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
|
|
5
|
+
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error";
|
|
5
6
|
export type ValidationFinding = {
|
|
6
7
|
severity: ValidationSeverity;
|
|
7
8
|
code: ValidationFindingCode | string;
|
|
@@ -11,12 +12,12 @@ export type ValidationFinding = {
|
|
|
11
12
|
pipelineId?: string;
|
|
12
13
|
stageId?: string;
|
|
13
14
|
};
|
|
14
|
-
export type ValidationScope = "full" | "pipeline";
|
|
15
|
+
export type ValidationScope = "full" | "pipeline" | "task";
|
|
15
16
|
export type ValidateCatalogOptions = {
|
|
16
17
|
cwd?: string;
|
|
17
|
-
stagesDir?: string;
|
|
18
18
|
scope: ValidationScope;
|
|
19
19
|
pipeline?: string;
|
|
20
|
+
task?: string;
|
|
20
21
|
strict?: boolean;
|
|
21
22
|
};
|
|
22
23
|
export type ValidationResult = {
|
|
@@ -33,4 +34,21 @@ export declare function effectiveSeverity(finding: ValidationFinding, strict: bo
|
|
|
33
34
|
export declare function buildValidationResult(scope: ValidationScope, findings: ValidationFinding[], strict: boolean): ValidationResult;
|
|
34
35
|
export declare function findingsFromLoadIssues(cwd: string, absPath: string, issues: LoadIssue[]): ValidationFinding[];
|
|
35
36
|
export declare function findingStageIdFilenameMismatch(cwd: string, absPath: string, fileStem: string, declaredId: string): ValidationFinding;
|
|
37
|
+
export type ValidatePipelineOptions = {
|
|
38
|
+
cwd?: string;
|
|
39
|
+
validateStages?: boolean;
|
|
40
|
+
strict?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare function validatePipeline(nameOrPath: string, options?: ValidatePipelineOptions): Promise<ValidationResult>;
|
|
43
|
+
export type LoadPipelineValidatedResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
loaded: LoadedPipeline;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
findings: ValidationFinding[];
|
|
49
|
+
};
|
|
50
|
+
export declare function loadPipelineValidated(nameOrPath: string, options?: {
|
|
51
|
+
cwd?: string;
|
|
52
|
+
validateStages?: boolean;
|
|
53
|
+
}): Promise<LoadPipelineValidatedResult>;
|
|
36
54
|
export declare function validateCatalog(options: ValidateCatalogOptions): Promise<ValidationResult>;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { loadPipelineOutcome,
|
|
2
|
+
import { loadPipelineOutcome, resolvePipelinePath } from "./loadPipeline.js";
|
|
4
3
|
import { loadStageOutcome } from "./loadStage.js";
|
|
4
|
+
import { loadTaskOutcome } from "./loadTask.js";
|
|
5
5
|
import { readYamlObject } from "./readYamlObject.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveCatalogContext } from "./resolveCatalogContext.js";
|
|
7
|
+
import { getCatalogScanPaths } from "./browseCatalog.js";
|
|
8
|
+
import { manifestPathForProject } from "./loadStageflowManifest.js";
|
|
7
9
|
export function relPath(cwd, absPath) {
|
|
8
|
-
return path.relative(cwd, absPath);
|
|
10
|
+
return path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
9
11
|
}
|
|
12
|
+
const STRICT_CATALOG_WARNINGS = new Set([
|
|
13
|
+
"catalog.manifest_missing",
|
|
14
|
+
"catalog.empty_catalog",
|
|
15
|
+
]);
|
|
10
16
|
const SEVERITY_RANK = {
|
|
11
17
|
error: 0,
|
|
12
18
|
warning: 1,
|
|
@@ -28,7 +34,7 @@ function sortFindings(findings) {
|
|
|
28
34
|
export function effectiveSeverity(finding, strict) {
|
|
29
35
|
if (strict &&
|
|
30
36
|
finding.severity === "warning" &&
|
|
31
|
-
finding.code
|
|
37
|
+
STRICT_CATALOG_WARNINGS.has(finding.code)) {
|
|
32
38
|
return "error";
|
|
33
39
|
}
|
|
34
40
|
return finding.severity;
|
|
@@ -82,6 +88,24 @@ function findingStageError(cwd, absPath, message, code, stageId) {
|
|
|
82
88
|
stageId,
|
|
83
89
|
}, "error");
|
|
84
90
|
}
|
|
91
|
+
function findingTaskError(cwd, absPath, message, code) {
|
|
92
|
+
return baseFinding({
|
|
93
|
+
cwd,
|
|
94
|
+
absPath,
|
|
95
|
+
message,
|
|
96
|
+
code,
|
|
97
|
+
category: "task",
|
|
98
|
+
}, "error");
|
|
99
|
+
}
|
|
100
|
+
function findingCatalog(cwd, absPath, message, code, severity) {
|
|
101
|
+
return baseFinding({
|
|
102
|
+
cwd,
|
|
103
|
+
absPath,
|
|
104
|
+
message,
|
|
105
|
+
code,
|
|
106
|
+
category: "catalog",
|
|
107
|
+
}, severity);
|
|
108
|
+
}
|
|
85
109
|
export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
86
110
|
return issues.flatMap((issue) => {
|
|
87
111
|
if (issue.category === "pipeline") {
|
|
@@ -94,6 +118,18 @@ export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
|
94
118
|
findingStageError(cwd, absPath, issue.message, issue.code, issue.stageId),
|
|
95
119
|
];
|
|
96
120
|
}
|
|
121
|
+
if (issue.category === "task") {
|
|
122
|
+
return [
|
|
123
|
+
findingTaskError(cwd, absPath, issue.message, issue.code),
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (issue.category === "catalog") {
|
|
127
|
+
const code = issue.code;
|
|
128
|
+
const severity = code === "catalog.manifest_missing" || code === "catalog.empty_catalog"
|
|
129
|
+
? "warning"
|
|
130
|
+
: "error";
|
|
131
|
+
return [findingCatalog(cwd, absPath, issue.message, code, severity)];
|
|
132
|
+
}
|
|
97
133
|
return [];
|
|
98
134
|
});
|
|
99
135
|
}
|
|
@@ -107,40 +143,6 @@ export function findingStageIdFilenameMismatch(cwd, absPath, fileStem, declaredI
|
|
|
107
143
|
stageId: declaredId,
|
|
108
144
|
}, "error");
|
|
109
145
|
}
|
|
110
|
-
function findingCatalogDuplicatePipelineId(cwd, absPath, pipelineId, conflictingPaths) {
|
|
111
|
-
const relConflicts = conflictingPaths.map((p) => relPath(cwd, p));
|
|
112
|
-
return baseFinding({
|
|
113
|
-
cwd,
|
|
114
|
-
absPath,
|
|
115
|
-
message: `Duplicate pipeline id "${pipelineId}" declared in: ${relConflicts.join(", ")}`,
|
|
116
|
-
code: "catalog.duplicate_pipeline_id",
|
|
117
|
-
category: "catalog",
|
|
118
|
-
pipelineId,
|
|
119
|
-
}, "error");
|
|
120
|
-
}
|
|
121
|
-
function findingCatalogOrphanStage(cwd, absPath, stageId) {
|
|
122
|
-
return baseFinding({
|
|
123
|
-
cwd,
|
|
124
|
-
absPath,
|
|
125
|
-
message: `Stage "${stageId}" is not referenced by any pipeline`,
|
|
126
|
-
code: "catalog.orphan_stage",
|
|
127
|
-
category: "catalog",
|
|
128
|
-
stageId,
|
|
129
|
-
}, "warning");
|
|
130
|
-
}
|
|
131
|
-
async function listYamlFiles(dir) {
|
|
132
|
-
let entries;
|
|
133
|
-
try {
|
|
134
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
return entries
|
|
140
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".yaml"))
|
|
141
|
-
.map((entry) => path.join(dir, entry.name))
|
|
142
|
-
.sort((a, b) => a.localeCompare(b));
|
|
143
|
-
}
|
|
144
146
|
async function checkStageIdFilename(cwd, stagePath) {
|
|
145
147
|
const fileStem = path.basename(stagePath, ".yaml");
|
|
146
148
|
try {
|
|
@@ -169,52 +171,79 @@ async function validateStageFile(cwd, stagePath) {
|
|
|
169
171
|
}
|
|
170
172
|
return findings;
|
|
171
173
|
}
|
|
172
|
-
async function
|
|
173
|
-
const { cwd,
|
|
174
|
-
|
|
175
|
-
const referencedStageIds = new Set();
|
|
176
|
-
let pipelineId;
|
|
174
|
+
async function runPipelineValidation(nameOrPath, options) {
|
|
175
|
+
const { cwd, validateStages } = options;
|
|
176
|
+
let pipelinePath;
|
|
177
177
|
try {
|
|
178
|
-
|
|
179
|
-
if (typeof raw?.id === "string") {
|
|
180
|
-
pipelineId = raw.id;
|
|
181
|
-
if (Array.isArray(raw.stages)) {
|
|
182
|
-
const stageIds = extractPipelineStageIds(raw.stages);
|
|
183
|
-
if (stageIds) {
|
|
184
|
-
for (const stageId of stageIds) {
|
|
185
|
-
referencedStageIds.add(stageId);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
178
|
+
pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
|
|
190
179
|
}
|
|
191
|
-
catch {
|
|
192
|
-
|
|
180
|
+
catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
|
|
185
|
+
{
|
|
186
|
+
code: "pipeline.load_error",
|
|
187
|
+
message,
|
|
188
|
+
category: "pipeline",
|
|
189
|
+
},
|
|
190
|
+
]),
|
|
191
|
+
};
|
|
193
192
|
}
|
|
194
|
-
const outcome = await loadPipelineOutcome(pipelinePath, { cwd
|
|
193
|
+
const outcome = await loadPipelineOutcome(pipelinePath, { cwd });
|
|
194
|
+
const findings = [];
|
|
195
195
|
if (!outcome.ok) {
|
|
196
196
|
findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
return { ok: false, findings };
|
|
198
|
+
}
|
|
199
|
+
if (validateStages && outcome.value.stageSources) {
|
|
200
|
+
for (const source of Object.values(outcome.value.stageSources)) {
|
|
201
|
+
if (source.kind === "file") {
|
|
202
|
+
findings.push(...(await validateStageFile(cwd, source.path)));
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
|
-
return { findings, pipelineId, referencedStageIds };
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (validateStages) {
|
|
208
|
-
const stagePath = path.join(stagesDir, `${stageId}.yaml`);
|
|
209
|
-
findings.push(...(await validateStageFile(cwd, stagePath)));
|
|
210
|
-
}
|
|
206
|
+
if (findings.some((finding) => finding.severity === "error")) {
|
|
207
|
+
return { ok: false, findings };
|
|
211
208
|
}
|
|
212
|
-
return {
|
|
209
|
+
return { ok: true, loaded: outcome.value, findings };
|
|
210
|
+
}
|
|
211
|
+
export async function validatePipeline(nameOrPath, options = {}) {
|
|
212
|
+
const cwd = options.cwd ?? process.cwd();
|
|
213
|
+
const validateStages = options.validateStages ?? true;
|
|
214
|
+
const strict = options.strict ?? false;
|
|
215
|
+
const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
|
|
216
|
+
return buildValidationResult("pipeline", core.findings, strict);
|
|
213
217
|
}
|
|
214
|
-
async function
|
|
215
|
-
const
|
|
218
|
+
export async function loadPipelineValidated(nameOrPath, options = {}) {
|
|
219
|
+
const cwd = options.cwd ?? process.cwd();
|
|
220
|
+
const validateStages = options.validateStages ?? true;
|
|
221
|
+
const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
|
|
222
|
+
if (core.ok) {
|
|
223
|
+
return { ok: true, loaded: core.loaded };
|
|
224
|
+
}
|
|
225
|
+
return { ok: false, findings: core.findings };
|
|
226
|
+
}
|
|
227
|
+
function findingCatalogDuplicatePipelineId(cwd, absPath, pipelineId, conflictingPaths) {
|
|
228
|
+
const relConflicts = conflictingPaths.map((p) => relPath(cwd, p));
|
|
229
|
+
return baseFinding({
|
|
230
|
+
cwd,
|
|
231
|
+
absPath,
|
|
232
|
+
message: `Duplicate pipeline id "${pipelineId}" declared in: ${relConflicts.join(", ")}`,
|
|
233
|
+
code: "catalog.duplicate_pipeline_id",
|
|
234
|
+
category: "catalog",
|
|
235
|
+
pipelineId,
|
|
236
|
+
}, "error");
|
|
237
|
+
}
|
|
238
|
+
function findingManifestMissing(cwd, projectRoot) {
|
|
239
|
+
return findingCatalog(cwd, manifestPathForProject(projectRoot), "No stageflow.yaml manifest found at project root", "catalog.manifest_missing", "warning");
|
|
240
|
+
}
|
|
241
|
+
function findingEmptyCatalog(cwd, manifestPath, kind) {
|
|
242
|
+
return findingCatalog(cwd, manifestPath, `Manifest catalog.${kind} is empty`, "catalog.empty_catalog", "warning");
|
|
243
|
+
}
|
|
244
|
+
async function collectPipelineIdsFromPaths(pipelinePaths) {
|
|
216
245
|
const idToPaths = new Map();
|
|
217
|
-
for (const filePath of
|
|
246
|
+
for (const filePath of pipelinePaths) {
|
|
218
247
|
try {
|
|
219
248
|
const raw = await readYamlObject(filePath);
|
|
220
249
|
if (typeof raw?.id !== "string")
|
|
@@ -240,83 +269,91 @@ function findingsForDuplicatePipelineIds(cwd, idToPaths) {
|
|
|
240
269
|
}
|
|
241
270
|
return findings;
|
|
242
271
|
}
|
|
243
|
-
async function
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (typeof raw?.id !== "string" || !Array.isArray(raw.stages))
|
|
250
|
-
continue;
|
|
251
|
-
const stageIds = extractPipelineStageIds(raw.stages);
|
|
252
|
-
if (!stageIds)
|
|
253
|
-
continue;
|
|
254
|
-
for (const stageId of stageIds) {
|
|
255
|
-
referenced.add(stageId);
|
|
256
|
-
}
|
|
272
|
+
async function validateManifestAll(invocationCwd) {
|
|
273
|
+
const ctx = await resolveCatalogContext(invocationCwd);
|
|
274
|
+
const findings = [];
|
|
275
|
+
if (ctx.manifestStatus === "not_git" || ctx.manifestStatus === "missing") {
|
|
276
|
+
if (ctx.projectRoot) {
|
|
277
|
+
findings.push(findingManifestMissing(invocationCwd, ctx.projectRoot));
|
|
257
278
|
}
|
|
258
|
-
|
|
259
|
-
|
|
279
|
+
else {
|
|
280
|
+
findings.push(findingCatalog(invocationCwd, path.resolve(invocationCwd), "Not inside a git repository; no catalog manifest", "catalog.manifest_missing", "warning"));
|
|
260
281
|
}
|
|
282
|
+
return findings;
|
|
261
283
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
findings
|
|
272
|
-
|
|
284
|
+
if (ctx.manifestStatus === "invalid") {
|
|
285
|
+
const manifestPath = ctx.projectRoot
|
|
286
|
+
? manifestPathForProject(ctx.projectRoot)
|
|
287
|
+
: path.resolve(invocationCwd, "stageflow.yaml");
|
|
288
|
+
findings.push(...findingsFromLoadIssues(invocationCwd, manifestPath, ctx.issues));
|
|
289
|
+
return findings;
|
|
290
|
+
}
|
|
291
|
+
const { projectRoot, manifest } = ctx;
|
|
292
|
+
if (!projectRoot || !manifest) {
|
|
293
|
+
return findings;
|
|
294
|
+
}
|
|
295
|
+
if (manifest.manifest.catalog.pipelines.length === 0) {
|
|
296
|
+
findings.push(findingEmptyCatalog(invocationCwd, manifest.path, "pipelines"));
|
|
297
|
+
}
|
|
298
|
+
if (manifest.manifest.catalog.tasks.length === 0) {
|
|
299
|
+
findings.push(findingEmptyCatalog(invocationCwd, manifest.path, "tasks"));
|
|
300
|
+
}
|
|
301
|
+
const scanPaths = await getCatalogScanPaths(ctx);
|
|
302
|
+
if (!scanPaths) {
|
|
303
|
+
return findings;
|
|
304
|
+
}
|
|
305
|
+
const { pipelinePaths, taskPaths } = scanPaths;
|
|
306
|
+
for (const pipelinePath of pipelinePaths) {
|
|
307
|
+
const core = await runPipelineValidation(pipelinePath, {
|
|
308
|
+
cwd: projectRoot,
|
|
309
|
+
validateStages: true,
|
|
310
|
+
});
|
|
311
|
+
if (!core.ok) {
|
|
312
|
+
findings.push(...core.findings);
|
|
313
|
+
}
|
|
273
314
|
}
|
|
315
|
+
for (const taskPath of taskPaths) {
|
|
316
|
+
const outcome = await loadTaskOutcome(taskPath);
|
|
317
|
+
if (!outcome.ok) {
|
|
318
|
+
findings.push(...findingsFromLoadIssues(projectRoot, taskPath, outcome.issues));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const idToPaths = await collectPipelineIdsFromPaths(pipelinePaths);
|
|
322
|
+
findings.push(...findingsForDuplicatePipelineIds(projectRoot, idToPaths));
|
|
274
323
|
return findings;
|
|
275
324
|
}
|
|
325
|
+
async function validateSingleTask(invocationCwd, taskArg) {
|
|
326
|
+
const ctx = await resolveCatalogContext(invocationCwd);
|
|
327
|
+
const relCwd = ctx.projectRoot ?? invocationCwd;
|
|
328
|
+
const taskPath = path.resolve(invocationCwd, taskArg);
|
|
329
|
+
const outcome = await loadTaskOutcome(taskPath);
|
|
330
|
+
if (!outcome.ok) {
|
|
331
|
+
return findingsFromLoadIssues(relCwd, taskPath, outcome.issues);
|
|
332
|
+
}
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
276
335
|
export async function validateCatalog(options) {
|
|
277
336
|
const cwd = options.cwd ?? process.cwd();
|
|
278
|
-
const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
|
|
279
337
|
const strict = options.strict ?? false;
|
|
280
|
-
const pipelinesDir = path.join(cwd, "pipelines");
|
|
281
338
|
if (options.scope === "pipeline" && !options.pipeline) {
|
|
282
339
|
throw new Error("validateCatalog: pipeline is required when scope is \"pipeline\"");
|
|
283
340
|
}
|
|
341
|
+
if (options.scope === "task" && !options.task) {
|
|
342
|
+
throw new Error("validateCatalog: task is required when scope is \"task\"");
|
|
343
|
+
}
|
|
284
344
|
const allFindings = [];
|
|
285
345
|
if (options.scope === "pipeline") {
|
|
286
|
-
|
|
346
|
+
return validatePipeline(options.pipeline, {
|
|
287
347
|
cwd,
|
|
288
|
-
stagesDir,
|
|
289
348
|
validateStages: true,
|
|
349
|
+
strict,
|
|
290
350
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
351
|
+
}
|
|
352
|
+
else if (options.scope === "task") {
|
|
353
|
+
allFindings.push(...(await validateSingleTask(cwd, options.task)));
|
|
294
354
|
}
|
|
295
355
|
else {
|
|
296
|
-
|
|
297
|
-
const pipelineFiles = await listYamlFiles(pipelinesDir);
|
|
298
|
-
for (const pipelinePath of pipelineFiles) {
|
|
299
|
-
const result = await validatePipelineFile(pipelinePath, {
|
|
300
|
-
cwd,
|
|
301
|
-
stagesDir,
|
|
302
|
-
validateStages: false,
|
|
303
|
-
});
|
|
304
|
-
allFindings.push(...result.findings);
|
|
305
|
-
for (const stageId of result.referencedStageIds) {
|
|
306
|
-
referencedStageIds.add(stageId);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const validatedStages = new Set();
|
|
310
|
-
for (const stageId of referencedStageIds) {
|
|
311
|
-
if (validatedStages.has(stageId))
|
|
312
|
-
continue;
|
|
313
|
-
validatedStages.add(stageId);
|
|
314
|
-
const stagePath = path.join(stagesDir, `${stageId}.yaml`);
|
|
315
|
-
allFindings.push(...(await validateStageFile(cwd, stagePath)));
|
|
316
|
-
}
|
|
317
|
-
const idToPaths = await collectPipelineIds(pipelinesDir);
|
|
318
|
-
allFindings.push(...findingsForDuplicatePipelineIds(cwd, idToPaths));
|
|
319
|
-
allFindings.push(...(await findingsForOrphanStages(cwd, stagesDir, referencedStageIds)));
|
|
356
|
+
allFindings.push(...(await validateManifestAll(cwd)));
|
|
320
357
|
}
|
|
321
358
|
return buildValidationResult(options.scope, allFindings, strict);
|
|
322
359
|
}
|
package/dist/envelope/check.js
CHANGED
|
@@ -25,6 +25,13 @@ export function assertRequiredEnvelope(value) {
|
|
|
25
25
|
summary: record.summary,
|
|
26
26
|
artifacts: record.artifacts,
|
|
27
27
|
};
|
|
28
|
+
if (record.fork_choice !== undefined) {
|
|
29
|
+
if (!Array.isArray(record.fork_choice) ||
|
|
30
|
+
!record.fork_choice.every((item) => typeof item === "string")) {
|
|
31
|
+
throw new EnvelopeError("fork_choice must be an array of strings");
|
|
32
|
+
}
|
|
33
|
+
envelope.fork_choice = record.fork_choice;
|
|
34
|
+
}
|
|
28
35
|
if (record.payload !== undefined) {
|
|
29
36
|
if (record.payload === null ||
|
|
30
37
|
typeof record.payload !== "object" ||
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ForkEmitContext } from "../types/forkChoice.js";
|
|
2
|
+
export declare function assertForkEnvelope(params: unknown, forkEmitContext: ForkEmitContext): void;
|
|
3
|
+
export declare function normalizeForkChoice(raw: string[] | undefined, mode: "emit" | "stored", forkEmitContext?: ForkEmitContext): Set<string>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit mode rejects missing fork_choice on fork success; stored mode treats
|
|
3
|
+
* undefined as an empty choice set (scheduler defensive path).
|
|
4
|
+
*/
|
|
5
|
+
import { EnvelopeError } from "../types/envelope.js";
|
|
6
|
+
function assertForkChoiceLegality(choices, forkEmitContext) {
|
|
7
|
+
const { immediateSuccessorIds, forkShape } = forkEmitContext;
|
|
8
|
+
for (const id of choices) {
|
|
9
|
+
if (!immediateSuccessorIds.includes(id)) {
|
|
10
|
+
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (choices.length === 0) {
|
|
14
|
+
if (forkShape === null || !forkShape.allowNone) {
|
|
15
|
+
throw new EnvelopeError("fork_choice must not be empty");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (forkShape !== null && forkShape.cardinality === "one" && choices.length !== 1) {
|
|
19
|
+
throw new EnvelopeError("fork shape requires exactly one choice");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function assertForkEnvelope(params, forkEmitContext) {
|
|
23
|
+
const record = params;
|
|
24
|
+
if (record.status === "failure")
|
|
25
|
+
return;
|
|
26
|
+
if (record.fork_choice === undefined) {
|
|
27
|
+
throw new EnvelopeError("fork stage must include fork_choice on success");
|
|
28
|
+
}
|
|
29
|
+
assertForkChoiceLegality(record.fork_choice, forkEmitContext);
|
|
30
|
+
}
|
|
31
|
+
export function normalizeForkChoice(raw, mode, forkEmitContext) {
|
|
32
|
+
if (mode === "stored") {
|
|
33
|
+
if (raw === undefined)
|
|
34
|
+
return new Set();
|
|
35
|
+
return new Set(raw);
|
|
36
|
+
}
|
|
37
|
+
if (forkEmitContext === undefined) {
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return new Set();
|
|
40
|
+
return new Set(raw);
|
|
41
|
+
}
|
|
42
|
+
if (raw === undefined) {
|
|
43
|
+
throw new EnvelopeError("fork stage must include fork_choice on success");
|
|
44
|
+
}
|
|
45
|
+
assertForkChoiceLegality(raw, forkEmitContext);
|
|
46
|
+
return new Set(raw);
|
|
47
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/findProjectRoot.js";
|
|
2
|
+
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
|
+
export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
|
+
export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
|
|
1
5
|
export { PACKAGE_NAME } from "./package-meta.js";
|
|
2
6
|
export type { StageEnvelope } from "./types/envelope.js";
|
|
3
7
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/findProjectRoot.js";
|
|
2
|
+
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
|
+
export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
|
+
export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
|
|
1
5
|
export { PACKAGE_NAME } from "./package-meta.js";
|
|
2
6
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
3
7
|
export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
|
package/dist/mcp/projectRun.d.ts
CHANGED
package/dist/mcp/projectRun.js
CHANGED
|
@@ -3,6 +3,13 @@ export function projectRunForMcp(detail) {
|
|
|
3
3
|
run_id: detail.run_id,
|
|
4
4
|
pipeline_id: detail.pipeline_id,
|
|
5
5
|
task_id: detail.task_id,
|
|
6
|
+
...(detail.pipeline_path !== undefined
|
|
7
|
+
? { pipeline_path: detail.pipeline_path }
|
|
8
|
+
: {}),
|
|
9
|
+
...(detail.task_path !== undefined ? { task_path: detail.task_path } : {}),
|
|
10
|
+
...(detail.project_root !== undefined
|
|
11
|
+
? { project_root: detail.project_root }
|
|
12
|
+
: {}),
|
|
6
13
|
status: detail.status,
|
|
7
14
|
created_at: detail.created_at,
|
|
8
15
|
updated_at: detail.updated_at,
|
package/dist/mcp/server.js
CHANGED
package/dist/mcp/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { browseCatalog } from "../config/browseCatalog.js";
|
|
3
3
|
import { projectRunForMcp } from "./projectRun.js";
|
|
4
4
|
import { readRunArtifact } from "./readArtifact.js";
|
|
5
5
|
const taskFileSchema = z.object({
|
|
@@ -9,6 +9,15 @@ const taskFileSchema = z.object({
|
|
|
9
9
|
constraints: z.string().optional(),
|
|
10
10
|
checkout: z.string().optional(),
|
|
11
11
|
});
|
|
12
|
+
const startRunSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
|
|
15
|
+
task_path: z.string().optional(),
|
|
16
|
+
task: taskFileSchema.optional(),
|
|
17
|
+
})
|
|
18
|
+
.refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
|
|
19
|
+
message: "Exactly one of task_path or task is required",
|
|
20
|
+
});
|
|
12
21
|
function textResult(data, isError = false) {
|
|
13
22
|
return {
|
|
14
23
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -18,11 +27,18 @@ function textResult(data, isError = false) {
|
|
|
18
27
|
export function registerMcpTools(server, deps) {
|
|
19
28
|
const { manager, store, cwd } = deps;
|
|
20
29
|
server.registerTool("list_pipelines", {
|
|
21
|
-
description: "List
|
|
30
|
+
description: "List manifest-declared pipeline paths from the project catalog",
|
|
31
|
+
inputSchema: z.object({}),
|
|
32
|
+
}, async () => {
|
|
33
|
+
const catalog = await browseCatalog(cwd);
|
|
34
|
+
return textResult({ pipelines: catalog.pipelines });
|
|
35
|
+
});
|
|
36
|
+
server.registerTool("list_tasks", {
|
|
37
|
+
description: "List manifest-declared task paths from the project catalog",
|
|
22
38
|
inputSchema: z.object({}),
|
|
23
39
|
}, async () => {
|
|
24
|
-
const
|
|
25
|
-
return textResult({
|
|
40
|
+
const catalog = await browseCatalog(cwd);
|
|
41
|
+
return textResult({ tasks: catalog.tasks });
|
|
26
42
|
});
|
|
27
43
|
server.registerTool("list_runs", {
|
|
28
44
|
description: "List known pipeline runs",
|
|
@@ -36,16 +52,17 @@ export function registerMcpTools(server, deps) {
|
|
|
36
52
|
inputSchema: z.object({}),
|
|
37
53
|
}, async () => textResult(manager.getHealth()));
|
|
38
54
|
server.registerTool("start_run", {
|
|
39
|
-
description: "Start a pipeline
|
|
40
|
-
inputSchema:
|
|
41
|
-
|
|
42
|
-
task: taskFileSchema,
|
|
43
|
-
}),
|
|
44
|
-
}, async ({ pipeline, task }) => {
|
|
55
|
+
description: "Start a pipeline run using a filesystem pipeline path and either task_path (catalog task file) or an inline task object. Returns { runId }. On conflict returns isError with code busy_capacity (soft max full) or busy_checkout (same checkout leased), plus activeCount/maxConcurrent/activeRunIds and optional conflictingRunId/conflictingCheckout.",
|
|
56
|
+
inputSchema: startRunSchema,
|
|
57
|
+
}, async ({ pipeline, task_path, task }) => {
|
|
45
58
|
if (!pipeline.trim()) {
|
|
46
59
|
return textResult({ error: "pipeline is required" }, true);
|
|
47
60
|
}
|
|
48
|
-
const
|
|
61
|
+
const taskInput = task_path ?? task;
|
|
62
|
+
if (taskInput === undefined) {
|
|
63
|
+
return textResult({ error: "Exactly one of task_path or task is required" }, true);
|
|
64
|
+
}
|
|
65
|
+
const result = await manager.startRun({ pipeline, task: taskInput });
|
|
49
66
|
if (!result.ok) {
|
|
50
67
|
const { ok: _ok, reason, ...rest } = result;
|
|
51
68
|
return textResult({ error: reason, ...rest }, true);
|