stageflow 0.3.0 → 0.9.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 +44 -14
- package/dist/agent/activity.d.ts +2 -0
- package/dist/agent/cursorProvider.d.ts +1 -10
- package/dist/agent/cursorProvider.js +18 -10
- package/dist/agent/fakeAgent.js +11 -7
- package/dist/agent/piAdapter.d.ts +2 -1
- package/dist/agent/piAdapter.js +28 -25
- package/dist/agent/port.d.ts +6 -1
- package/dist/agent/port.js +3 -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/runCommand.d.ts +5 -1
- package/dist/cli/runCommand.js +36 -5
- package/dist/cli/runOutput.d.ts +4 -1
- package/dist/cli/runOutput.js +36 -8
- package/dist/cli/runsCommand.d.ts +22 -0
- package/dist/cli/runsCommand.js +591 -0
- package/dist/cli/skillsCommand.d.ts +16 -0
- package/dist/cli/skillsCommand.js +459 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.js +110 -5
- package/dist/config/loadPipeline.js +5 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
- package/dist/config/normalizePipelineStageEntry.js +25 -1
- package/dist/config/pipelineStageKeys.js +1 -1
- package/dist/config/resolveForkEmitContext.d.ts +2 -1
- package/dist/config/resolveForkEmitContext.js +23 -1
- package/dist/config/resolvePipelineDag.js +36 -4
- package/dist/envelope/check.d.ts +2 -0
- package/dist/envelope/check.js +51 -0
- package/dist/envelope/cloneForks.d.ts +2 -0
- package/dist/envelope/cloneForks.js +62 -0
- package/dist/envelope/forkChoice.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- 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 -40
- package/dist/mcp/projectRun.js +1 -64
- 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 -103
- 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/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -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/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 +13 -2
- package/dist/runstore/port.js +4 -1
- package/dist/runstore/runProjection.d.ts +1 -1
- package/dist/runstore/runProjection.js +14 -3
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
- package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
- 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/envelopeRouting.d.ts +2 -1
- package/dist/runtime/envelopeRouting.js +80 -13
- package/dist/runtime/pipelineScheduler.d.ts +6 -2
- package/dist/runtime/pipelineScheduler.js +166 -30
- package/dist/runtime/resumeReconstruct.js +8 -3
- package/dist/runtime/runChangeBus.d.ts +18 -0
- package/dist/runtime/runChangeBus.js +83 -0
- package/dist/runtime/runManager.d.ts +7 -0
- package/dist/runtime/runManager.js +31 -2
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +4 -1
- package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
- package/dist/runtime/stageAttemptBootstrap.js +17 -9
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +17 -15
- package/dist/runtime/stageWorker.js +11 -4
- 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 +295 -374
- 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 +11 -2
- package/dist/tools/emitStageEnvelope.js +22 -6
- package/dist/types/envelope.d.ts +2 -0
- package/dist/types/forkChoice.d.ts +22 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +118 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/install-suite.sh +107 -0
- package/skills/stageflow/SKILL.md +26 -0
- package/skills/stageflow/references/control-surface.md +28 -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 +182 -0
- package/skills/stageflow-run/references/mcp-call.md +17 -0
- package/skills/stageflow-run/references/native-question-ui.md +78 -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-CFSzDZje.js +0 -118
|
@@ -28,7 +28,21 @@ function extractBodyRaw(raw) {
|
|
|
28
28
|
return body;
|
|
29
29
|
}
|
|
30
30
|
function hasBodyKey(raw) {
|
|
31
|
-
return Object.keys(raw).some((key) => BODY_KEYS.has(key));
|
|
31
|
+
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill");
|
|
32
|
+
}
|
|
33
|
+
function readSkill(raw) {
|
|
34
|
+
if (raw.skill === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof raw.skill !== "string" || raw.skill.trim() === "") {
|
|
37
|
+
return loadFailure([
|
|
38
|
+
{
|
|
39
|
+
code: "stage.invalid_skill",
|
|
40
|
+
message: "skill must be a non-empty string",
|
|
41
|
+
category: "stage",
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
return loadSuccess(raw.skill.trim());
|
|
32
46
|
}
|
|
33
47
|
function stringStageRefMessage(index, declaringPath, entry) {
|
|
34
48
|
const hint = entry
|
|
@@ -74,6 +88,11 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
74
88
|
}
|
|
75
89
|
const uses = typeof raw.uses === "string" ? raw.uses : undefined;
|
|
76
90
|
const hasBody = hasBodyKey(raw);
|
|
91
|
+
const skillOutcome = readSkill(raw);
|
|
92
|
+
if (skillOutcome !== undefined && !skillOutcome.ok) {
|
|
93
|
+
return skillOutcome;
|
|
94
|
+
}
|
|
95
|
+
const skill = skillOutcome?.ok ? skillOutcome.value : undefined;
|
|
77
96
|
if (uses && hasBody) {
|
|
78
97
|
return loadFailure([
|
|
79
98
|
{
|
|
@@ -161,6 +180,9 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
161
180
|
body,
|
|
162
181
|
...(typeof raw.needs === "string" ? { needs: raw.needs } : {}),
|
|
163
182
|
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
183
|
+
...(raw.clonable !== undefined ? { clonable: raw.clonable } : {}),
|
|
184
|
+
...(raw.clone_cap !== undefined ? { clone_cap: raw.clone_cap } : {}),
|
|
185
|
+
...(skill !== undefined ? { skill } : {}),
|
|
164
186
|
};
|
|
165
187
|
const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
|
|
166
188
|
if (priorPath) {
|
|
@@ -182,5 +204,7 @@ export function toWiringRefs(entries) {
|
|
|
182
204
|
id: entry.id,
|
|
183
205
|
...(entry.needs !== undefined ? { needs: entry.needs } : {}),
|
|
184
206
|
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
207
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
208
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
185
209
|
}));
|
|
186
210
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ForkEmitContext } from "../types/forkChoice.js";
|
|
1
|
+
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
2
2
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
3
|
+
export declare function resolveCloneEmitContext(dag: ResolvedPipelineDag, stageId: string): CloneEmitContext | undefined;
|
|
3
4
|
export declare function resolveForkEmitContext(dag: ResolvedPipelineDag, stageId: string): ForkEmitContext | undefined;
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
export function resolveCloneEmitContext(dag, stageId) {
|
|
2
|
+
const childIds = dag.childrenOf[stageId] ?? [];
|
|
3
|
+
const clonableSuccessors = [];
|
|
4
|
+
for (const childId of childIds) {
|
|
5
|
+
const node = dag.nodes.find((n) => n.id === childId);
|
|
6
|
+
if (node?.clonable === true) {
|
|
7
|
+
clonableSuccessors.push({
|
|
8
|
+
successorId: childId,
|
|
9
|
+
cloneCap: node.clone_cap,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (clonableSuccessors.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { clonableSuccessors };
|
|
16
|
+
}
|
|
1
17
|
export function resolveForkEmitContext(dag, stageId) {
|
|
2
18
|
const dagNode = dag.nodes.find((n) => n.id === stageId);
|
|
3
19
|
if (!dagNode?.fork)
|
|
4
20
|
return undefined;
|
|
21
|
+
const immediateSuccessorIds = (dag.childrenOf[stageId] ?? []).filter((childId) => {
|
|
22
|
+
const child = dag.nodes.find((n) => n.id === childId);
|
|
23
|
+
return child?.clonable !== true;
|
|
24
|
+
});
|
|
25
|
+
if (immediateSuccessorIds.length === 0)
|
|
26
|
+
return undefined;
|
|
5
27
|
return {
|
|
6
|
-
immediateSuccessorIds
|
|
28
|
+
immediateSuccessorIds,
|
|
7
29
|
forkShape: {
|
|
8
30
|
cardinality: dagNode.fork.select,
|
|
9
31
|
allowNone: dagNode.fork.allow_none,
|
|
@@ -49,8 +49,16 @@ export function parsePipelineStageEntries(raw, ctx) {
|
|
|
49
49
|
}
|
|
50
50
|
forkValue = entry.fork;
|
|
51
51
|
}
|
|
52
|
+
const clonableFields = {
|
|
53
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
54
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
55
|
+
};
|
|
52
56
|
if (entry.needs === undefined) {
|
|
53
|
-
entries.push(
|
|
57
|
+
entries.push({
|
|
58
|
+
id: entry.id,
|
|
59
|
+
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
60
|
+
...clonableFields,
|
|
61
|
+
});
|
|
54
62
|
continue;
|
|
55
63
|
}
|
|
56
64
|
if (Array.isArray(entry.needs)) {
|
|
@@ -62,9 +70,12 @@ export function parsePipelineStageEntries(raw, ctx) {
|
|
|
62
70
|
if (typeof entry.needs !== "string" || !entry.needs) {
|
|
63
71
|
throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a non-empty string`));
|
|
64
72
|
}
|
|
65
|
-
entries.push(
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
entries.push({
|
|
74
|
+
id: entry.id,
|
|
75
|
+
needs: entry.needs,
|
|
76
|
+
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
77
|
+
...clonableFields,
|
|
78
|
+
});
|
|
68
79
|
}
|
|
69
80
|
return entries;
|
|
70
81
|
}
|
|
@@ -74,6 +85,8 @@ function normalizeToEdges(entries) {
|
|
|
74
85
|
needs: entry.needs ?? null,
|
|
75
86
|
stageIndex: index,
|
|
76
87
|
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
88
|
+
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
89
|
+
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
77
90
|
}));
|
|
78
91
|
}
|
|
79
92
|
function detectDuplicateIds(edges, ctx) {
|
|
@@ -213,6 +226,9 @@ function buildResolvedPipelineDag(edges) {
|
|
|
213
226
|
...(edge.fork !== undefined
|
|
214
227
|
? { fork: { select: edge.fork.select, allow_none: edge.fork.allow_none ?? false } }
|
|
215
228
|
: {}),
|
|
229
|
+
...(edge.clonable === true
|
|
230
|
+
? { clonable: true, clone_cap: edge.clone_cap ?? 5 }
|
|
231
|
+
: {}),
|
|
216
232
|
}));
|
|
217
233
|
return { nodes, roots, childrenOf };
|
|
218
234
|
}
|
|
@@ -228,6 +244,21 @@ function validateForkFields(edges, dag, ctx) {
|
|
|
228
244
|
}
|
|
229
245
|
}
|
|
230
246
|
}
|
|
247
|
+
function validateClonableFields(edges, dag, ctx) {
|
|
248
|
+
for (const edge of edges) {
|
|
249
|
+
if (edge.clone_cap !== undefined && edge.clonable !== true) {
|
|
250
|
+
throw new Error(formatError(ctx, `stage "${edge.id}": clone_cap requires clonable: true`));
|
|
251
|
+
}
|
|
252
|
+
if (edge.clonable === true && edge.clone_cap !== undefined) {
|
|
253
|
+
if (!Number.isInteger(edge.clone_cap) || edge.clone_cap < 2) {
|
|
254
|
+
throw new Error(formatError(ctx, `stage "${edge.id}": clone_cap must be an integer greater than or equal to 2`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (edge.clonable === true && (dag.childrenOf[edge.id] ?? []).length === 0) {
|
|
258
|
+
throw new Error(formatError(ctx, `clonable on stage "${edge.id}": no children in the DAG`));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
231
262
|
export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
232
263
|
const edges = normalizeToEdges(refs);
|
|
233
264
|
detectDuplicateIds(edges, ctx);
|
|
@@ -239,6 +270,7 @@ export function resolvePipelineDagFromRefs(refs, ctx) {
|
|
|
239
270
|
.map((edge) => edge.id);
|
|
240
271
|
const dag = buildResolvedPipelineDag(edges);
|
|
241
272
|
validateForkFields(edges, dag, ctx);
|
|
273
|
+
validateClonableFields(edges, dag, ctx);
|
|
242
274
|
return { stages, dag };
|
|
243
275
|
}
|
|
244
276
|
export function resolvePipelineDag(rawStages, ctx) {
|
package/dist/envelope/check.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { type StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { CloneForkItem } from "../types/forkChoice.js";
|
|
3
|
+
export declare function parseCloneForks(raw: unknown): CloneForkItem[];
|
|
2
4
|
export declare function assertRequiredEnvelope(value: unknown): StageEnvelope;
|
|
3
5
|
export declare function isAdvancingEnvelope(envelope: StageEnvelope): boolean;
|
package/dist/envelope/check.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { EnvelopeError, } from "../types/envelope.js";
|
|
2
|
+
function parseCloneForkItem(raw, index) {
|
|
3
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4
|
+
throw new EnvelopeError(`clone_forks[${index}] must be an object`);
|
|
5
|
+
}
|
|
6
|
+
const record = raw;
|
|
7
|
+
if (typeof record.successor_id !== "string") {
|
|
8
|
+
throw new EnvelopeError(`clone_forks[${index}].successor_id must be a string`);
|
|
9
|
+
}
|
|
10
|
+
const action = record.action;
|
|
11
|
+
if (action !== "skip" && action !== "once" && action !== "fanout") {
|
|
12
|
+
throw new EnvelopeError(`clone_forks[${index}].action must be "skip", "once", or "fanout"`);
|
|
13
|
+
}
|
|
14
|
+
const item = {
|
|
15
|
+
successor_id: record.successor_id,
|
|
16
|
+
action,
|
|
17
|
+
};
|
|
18
|
+
if (record.envelope !== undefined) {
|
|
19
|
+
item.envelope = assertRequiredEnvelope(record.envelope);
|
|
20
|
+
}
|
|
21
|
+
if (record.mode !== undefined) {
|
|
22
|
+
if (record.mode !== "parallel" && record.mode !== "sequential") {
|
|
23
|
+
throw new EnvelopeError(`clone_forks[${index}].mode must be "parallel" or "sequential"`);
|
|
24
|
+
}
|
|
25
|
+
item.mode = record.mode;
|
|
26
|
+
}
|
|
27
|
+
if (record.clones !== undefined) {
|
|
28
|
+
if (!Array.isArray(record.clones)) {
|
|
29
|
+
throw new EnvelopeError(`clone_forks[${index}].clones must be an array`);
|
|
30
|
+
}
|
|
31
|
+
item.clones = record.clones.map((clone, cloneIndex) => {
|
|
32
|
+
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
33
|
+
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}] must be an object`);
|
|
34
|
+
}
|
|
35
|
+
const cloneRecord = clone;
|
|
36
|
+
if (cloneRecord.envelope === undefined) {
|
|
37
|
+
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}].envelope is required`);
|
|
38
|
+
}
|
|
39
|
+
return { envelope: assertRequiredEnvelope(cloneRecord.envelope) };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
44
|
+
export function parseCloneForks(raw) {
|
|
45
|
+
if (!Array.isArray(raw)) {
|
|
46
|
+
throw new EnvelopeError("clone_forks must be an array");
|
|
47
|
+
}
|
|
48
|
+
return raw.map((item, index) => parseCloneForkItem(item, index));
|
|
49
|
+
}
|
|
2
50
|
export function assertRequiredEnvelope(value) {
|
|
3
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4
52
|
throw new EnvelopeError("envelope must be an object");
|
|
@@ -32,6 +80,9 @@ export function assertRequiredEnvelope(value) {
|
|
|
32
80
|
}
|
|
33
81
|
envelope.fork_choice = record.fork_choice;
|
|
34
82
|
}
|
|
83
|
+
if (record.clone_forks !== undefined) {
|
|
84
|
+
envelope.clone_forks = parseCloneForks(record.clone_forks);
|
|
85
|
+
}
|
|
35
86
|
if (record.payload !== undefined) {
|
|
36
87
|
if (record.payload === null ||
|
|
37
88
|
typeof record.payload !== "object" ||
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseCloneForks } from "./check.js";
|
|
2
|
+
import { EnvelopeError } from "../types/envelope.js";
|
|
3
|
+
function fieldPresent(item, key) {
|
|
4
|
+
return item[key] !== undefined;
|
|
5
|
+
}
|
|
6
|
+
export function assertCloneForks(params, cloneEmitContext) {
|
|
7
|
+
const record = params;
|
|
8
|
+
if (record.status === "failure")
|
|
9
|
+
return;
|
|
10
|
+
if (record.clone_forks === undefined || !Array.isArray(record.clone_forks)) {
|
|
11
|
+
throw new EnvelopeError("clonable successor requires clone_forks on success");
|
|
12
|
+
}
|
|
13
|
+
const items = parseCloneForks(record.clone_forks);
|
|
14
|
+
if (items.length === 0 && cloneEmitContext.clonableSuccessors.length > 0) {
|
|
15
|
+
throw new EnvelopeError("clone_forks must not be empty when clonable successors exist");
|
|
16
|
+
}
|
|
17
|
+
const expectedIds = cloneEmitContext.clonableSuccessors.map((s) => s.successorId);
|
|
18
|
+
const expectedSet = new Set(expectedIds);
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
if (seen.has(item.successor_id)) {
|
|
22
|
+
throw new EnvelopeError(`clone_forks contains duplicate successor_id: ${item.successor_id}`);
|
|
23
|
+
}
|
|
24
|
+
if (!expectedSet.has(item.successor_id)) {
|
|
25
|
+
throw new EnvelopeError(`clone_forks contains extra successor_id: ${item.successor_id}`);
|
|
26
|
+
}
|
|
27
|
+
seen.add(item.successor_id);
|
|
28
|
+
const capEntry = cloneEmitContext.clonableSuccessors.find((s) => s.successorId === item.successor_id);
|
|
29
|
+
const cloneCap = capEntry?.cloneCap;
|
|
30
|
+
if (item.action === "skip") {
|
|
31
|
+
if (fieldPresent(item, "envelope") ||
|
|
32
|
+
fieldPresent(item, "mode") ||
|
|
33
|
+
fieldPresent(item, "clones")) {
|
|
34
|
+
throw new EnvelopeError(`clone_forks skip for ${item.successor_id} must not include envelope, mode, or clones`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (item.action === "once") {
|
|
38
|
+
if (!fieldPresent(item, "envelope")) {
|
|
39
|
+
throw new EnvelopeError(`clone_forks once for ${item.successor_id} requires envelope`);
|
|
40
|
+
}
|
|
41
|
+
if (fieldPresent(item, "mode") || fieldPresent(item, "clones")) {
|
|
42
|
+
throw new EnvelopeError(`clone_forks once for ${item.successor_id} must not include mode or clones`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (item.action === "fanout") {
|
|
46
|
+
if (fieldPresent(item, "envelope")) {
|
|
47
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} must not include a top-level envelope`);
|
|
48
|
+
}
|
|
49
|
+
if (!fieldPresent(item, "mode") || !fieldPresent(item, "clones") || item.clones === undefined) {
|
|
50
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} requires mode and clones`);
|
|
51
|
+
}
|
|
52
|
+
if (cloneCap === undefined || item.clones.length < 2 || item.clones.length > cloneCap) {
|
|
53
|
+
throw new EnvelopeError(`clone_forks fanout for ${item.successor_id} must have between 2 and ${cloneCap} clones`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const id of expectedIds) {
|
|
58
|
+
if (!seen.has(id)) {
|
|
59
|
+
throw new EnvelopeError(`clone_forks missing successor_id: ${id}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -7,7 +7,7 @@ function assertForkChoiceLegality(choices, forkEmitContext) {
|
|
|
7
7
|
const { immediateSuccessorIds, forkShape } = forkEmitContext;
|
|
8
8
|
for (const id of choices) {
|
|
9
9
|
if (!immediateSuccessorIds.includes(id)) {
|
|
10
|
-
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}`);
|
|
10
|
+
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}; clonable successors belong in clone_forks`);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
if (choices.length === 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export type { StageEnvelope } from "./types/envelope.js";
|
|
7
7
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
8
8
|
export type { AgentPort } from "./agent/port.js";
|
|
@@ -15,3 +15,5 @@ export type { RunStore, RunSummary, RunDetail, RunMeta, RunStatus, StageSnapshot
|
|
|
15
15
|
export { deriveStatusFromStages } from "./runstore/port.js";
|
|
16
16
|
export { RunManager } from "./runtime/runManager.js";
|
|
17
17
|
export { startUiServer } from "./server/http.js";
|
|
18
|
+
export { startMcpServer } from "./server/mcpHost.js";
|
|
19
|
+
export { DEFAULT_PORT } from "./server/mcpHost.js";
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,12 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
7
7
|
export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
|
|
8
8
|
export { createRunStore } from "./runstore/createStore.js";
|
|
9
9
|
export { deriveStatusFromStages } from "./runstore/port.js";
|
|
10
10
|
export { RunManager } from "./runtime/runManager.js";
|
|
11
11
|
export { startUiServer } from "./server/http.js";
|
|
12
|
+
export { startMcpServer } from "./server/mcpHost.js";
|
|
13
|
+
export { DEFAULT_PORT } from "./server/mcpHost.js";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { browseCatalog } from "../config/browseCatalog.js";
|
|
3
|
+
import { loadPipeline } from "../config/loadPipeline.js";
|
|
4
|
+
import { validateCatalog } from "../config/validateCatalog.js";
|
|
5
|
+
import { projectRunForMcp } from "./projectRun.js";
|
|
6
|
+
import { readRunArtifact } from "./readArtifact.js";
|
|
7
|
+
import { textResult } from "./toolResults.js";
|
|
8
|
+
const taskFileSchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
goal: z.string(),
|
|
11
|
+
context: z.string().optional(),
|
|
12
|
+
constraints: z.string().optional(),
|
|
13
|
+
checkout: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
const startRunSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
|
|
18
|
+
task_path: z.string().optional(),
|
|
19
|
+
task: taskFileSchema.optional(),
|
|
20
|
+
})
|
|
21
|
+
.refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
|
|
22
|
+
message: "Exactly one of task_path or task is required",
|
|
23
|
+
});
|
|
24
|
+
const runStatusSchema = z.enum(["created", "running", "succeeded", "failed"]);
|
|
25
|
+
export function registerCatalogTools(server, deps) {
|
|
26
|
+
const { manager, store, cwd } = deps;
|
|
27
|
+
server.registerTool("list_pipelines", {
|
|
28
|
+
description: "List manifest-declared pipeline paths from the project catalog",
|
|
29
|
+
inputSchema: z.object({}),
|
|
30
|
+
}, async () => {
|
|
31
|
+
const catalog = await browseCatalog(cwd);
|
|
32
|
+
return textResult({ pipelines: catalog.pipelines });
|
|
33
|
+
});
|
|
34
|
+
server.registerTool("list_tasks", {
|
|
35
|
+
description: "List manifest-declared task paths from the project catalog",
|
|
36
|
+
inputSchema: z.object({}),
|
|
37
|
+
}, async () => {
|
|
38
|
+
const catalog = await browseCatalog(cwd);
|
|
39
|
+
return textResult({ tasks: catalog.tasks });
|
|
40
|
+
});
|
|
41
|
+
server.registerTool("list_runs", {
|
|
42
|
+
description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path).",
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
status: runStatusSchema.optional(),
|
|
45
|
+
since: z.string().optional(),
|
|
46
|
+
pipeline: z.string().optional(),
|
|
47
|
+
}),
|
|
48
|
+
}, async ({ status, since, pipeline }) => {
|
|
49
|
+
const filter = {};
|
|
50
|
+
if (status !== undefined)
|
|
51
|
+
filter.status = status;
|
|
52
|
+
if (since !== undefined) {
|
|
53
|
+
if (!Number.isFinite(Date.parse(since))) {
|
|
54
|
+
return textResult({ error: "since must be a valid date", status: 400 }, true);
|
|
55
|
+
}
|
|
56
|
+
filter.since = since;
|
|
57
|
+
}
|
|
58
|
+
if (pipeline !== undefined)
|
|
59
|
+
filter.pipeline = pipeline;
|
|
60
|
+
const runs = await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined);
|
|
61
|
+
return textResult({ runs });
|
|
62
|
+
});
|
|
63
|
+
server.registerTool("get_health", {
|
|
64
|
+
description: "Server health and soft-max run capacity: activeRunIds, activeCount, maxConcurrent, slotsAvailable. Start until slotsAvailable is 0; then wait for a run to finish or raise STAGEFLOW_MAX_CONCURRENT_RUNS.",
|
|
65
|
+
inputSchema: z.object({}),
|
|
66
|
+
}, async () => textResult(manager.getHealth()));
|
|
67
|
+
server.registerTool("start_run", {
|
|
68
|
+
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.",
|
|
69
|
+
inputSchema: startRunSchema,
|
|
70
|
+
}, async ({ pipeline, task_path, task }) => {
|
|
71
|
+
if (!pipeline.trim()) {
|
|
72
|
+
return textResult({ error: "pipeline is required" }, true);
|
|
73
|
+
}
|
|
74
|
+
const taskInput = task_path ?? task;
|
|
75
|
+
if (taskInput === undefined) {
|
|
76
|
+
return textResult({ error: "Exactly one of task_path or task is required" }, true);
|
|
77
|
+
}
|
|
78
|
+
const result = await manager.startRun({ pipeline, task: taskInput });
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
const { ok: _ok, reason, ...rest } = result;
|
|
81
|
+
return textResult({ error: reason, ...rest }, true);
|
|
82
|
+
}
|
|
83
|
+
return textResult({ runId: result.runId });
|
|
84
|
+
});
|
|
85
|
+
server.registerTool("get_run", {
|
|
86
|
+
description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events / get_envelope for timelines and full envelopes.",
|
|
87
|
+
inputSchema: z.object({
|
|
88
|
+
runId: z.string(),
|
|
89
|
+
}),
|
|
90
|
+
}, async ({ runId }) => {
|
|
91
|
+
try {
|
|
92
|
+
const detail = await store.readRun(runId);
|
|
93
|
+
return textResult(projectRunForMcp(detail));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
const notFound = /not found|no such|unknown run/i.test(message);
|
|
98
|
+
return textResult({
|
|
99
|
+
error: message,
|
|
100
|
+
status: notFound ? 404 : 500,
|
|
101
|
+
}, true);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
server.registerTool("read_artifact", {
|
|
105
|
+
description: "Read a text artifact from a run workspace by relative path (contained under the run workspace).",
|
|
106
|
+
inputSchema: z.object({
|
|
107
|
+
runId: z.string(),
|
|
108
|
+
path: z.string(),
|
|
109
|
+
}),
|
|
110
|
+
}, async ({ runId, path: artifactPath }) => {
|
|
111
|
+
try {
|
|
112
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
113
|
+
return textResult({ runId, path: artifactPath, content });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
const notFound = message.startsWith("Run not found") ||
|
|
118
|
+
message.startsWith("Artifact not found") ||
|
|
119
|
+
/no such|not found/i.test(message);
|
|
120
|
+
return textResult({
|
|
121
|
+
error: message,
|
|
122
|
+
status: notFound ? 404 : 400,
|
|
123
|
+
}, true);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
server.registerTool("validate", {
|
|
127
|
+
description: "Validate the project catalog (full), a pipeline path, or a task path. Returns ValidationResult JSON (ok, summary, findings).",
|
|
128
|
+
inputSchema: z.object({
|
|
129
|
+
pipeline: z.string().optional(),
|
|
130
|
+
task: z.string().optional(),
|
|
131
|
+
strict: z.boolean().optional(),
|
|
132
|
+
}),
|
|
133
|
+
}, async ({ pipeline, task, strict }) => {
|
|
134
|
+
try {
|
|
135
|
+
let scope = "full";
|
|
136
|
+
if (pipeline !== undefined && pipeline.trim())
|
|
137
|
+
scope = "pipeline";
|
|
138
|
+
else if (task !== undefined && task.trim())
|
|
139
|
+
scope = "task";
|
|
140
|
+
const result = await validateCatalog({
|
|
141
|
+
cwd,
|
|
142
|
+
scope,
|
|
143
|
+
...(scope === "pipeline" ? { pipeline: pipeline.trim() } : {}),
|
|
144
|
+
...(scope === "task" ? { task: task.trim() } : {}),
|
|
145
|
+
strict: strict ?? false,
|
|
146
|
+
});
|
|
147
|
+
return textResult(result);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
return textResult({ error: message, status: 400 }, true);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.registerTool("describe_pipeline", {
|
|
155
|
+
description: "Describe a pipeline DAG: stages with needs, fork, clonable, clone_cap, and gate_kinds. Input is a filesystem pipeline path (same as start_run).",
|
|
156
|
+
inputSchema: z.object({
|
|
157
|
+
pipeline: z.string(),
|
|
158
|
+
}),
|
|
159
|
+
}, async ({ pipeline }) => {
|
|
160
|
+
if (!pipeline.trim()) {
|
|
161
|
+
return textResult({ error: "pipeline is required", status: 400 }, true);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const loaded = await loadPipeline(pipeline.trim(), { cwd });
|
|
165
|
+
const gateById = new Map(loaded.stages.map((s) => [s.id, s.gate_kinds]));
|
|
166
|
+
const stages = loaded.dag.nodes.map((node) => ({
|
|
167
|
+
id: node.id,
|
|
168
|
+
needs: node.needs,
|
|
169
|
+
...(node.fork !== undefined ? { fork: node.fork } : {}),
|
|
170
|
+
...(node.clonable !== undefined ? { clonable: node.clonable } : {}),
|
|
171
|
+
...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
|
|
172
|
+
...(gateById.get(node.id) !== undefined
|
|
173
|
+
? { gate_kinds: gateById.get(node.id) }
|
|
174
|
+
: {}),
|
|
175
|
+
}));
|
|
176
|
+
return textResult({
|
|
177
|
+
id: loaded.pipeline.id,
|
|
178
|
+
path: loaded.pipelinePath,
|
|
179
|
+
stages,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return textResult({ error: message, status: 404 }, true);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|