stageflow 0.3.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 +22 -1
- 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/skillsCommand.d.ts +16 -0
- package/dist/cli/skillsCommand.js +459 -0
- package/dist/cli.js +78 -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 +2 -0
- package/dist/index.js +2 -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 -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/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/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 +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-CFSzDZje.js +0 -118
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildHandoffDeliverable, isHandoffSkipped, } from "./handoffFormat.js";
|
|
4
|
+
import { createRunStore } from "../runstore/createStore.js";
|
|
5
|
+
export const ENVELOPE_USAGE = `Usage:
|
|
6
|
+
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]`;
|
|
7
|
+
const defaultIo = {
|
|
8
|
+
log: (line) => console.log(line),
|
|
9
|
+
error: (line) => console.error(line),
|
|
10
|
+
};
|
|
11
|
+
export function parseEnvelopeArgs(args) {
|
|
12
|
+
if (args.length === 0) {
|
|
13
|
+
return { help: false, json: false, format: "envelope" };
|
|
14
|
+
}
|
|
15
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
16
|
+
return { help: true, json: false, format: "envelope" };
|
|
17
|
+
}
|
|
18
|
+
const subcommand = args[0];
|
|
19
|
+
if (subcommand !== "get") {
|
|
20
|
+
throw new Error(`Unknown envelope subcommand: ${subcommand}`);
|
|
21
|
+
}
|
|
22
|
+
let runId;
|
|
23
|
+
let stageId;
|
|
24
|
+
let json = false;
|
|
25
|
+
let fromPath;
|
|
26
|
+
let detectStageId;
|
|
27
|
+
let format = "envelope";
|
|
28
|
+
let help = false;
|
|
29
|
+
for (let i = 1; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === "--help" || arg === "-h") {
|
|
32
|
+
help = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === "--run") {
|
|
35
|
+
const value = args[++i];
|
|
36
|
+
if (value === undefined || value.length === 0) {
|
|
37
|
+
throw new Error("Missing value for --run");
|
|
38
|
+
}
|
|
39
|
+
runId = value;
|
|
40
|
+
}
|
|
41
|
+
else if (arg === "--stage") {
|
|
42
|
+
const value = args[++i];
|
|
43
|
+
if (value === undefined || value.length === 0) {
|
|
44
|
+
throw new Error("Missing value for --stage");
|
|
45
|
+
}
|
|
46
|
+
stageId = value;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === "--json") {
|
|
49
|
+
json = true;
|
|
50
|
+
}
|
|
51
|
+
else if (arg === "--from") {
|
|
52
|
+
const value = args[++i];
|
|
53
|
+
if (value === undefined || value.length === 0) {
|
|
54
|
+
throw new Error("Missing value for --from");
|
|
55
|
+
}
|
|
56
|
+
fromPath = value;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--detect-stage") {
|
|
59
|
+
const value = args[++i];
|
|
60
|
+
if (value === undefined || value.length === 0) {
|
|
61
|
+
throw new Error("Missing value for --detect-stage");
|
|
62
|
+
}
|
|
63
|
+
detectStageId = value;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--format") {
|
|
66
|
+
const value = args[++i];
|
|
67
|
+
if (value === undefined || value.length === 0) {
|
|
68
|
+
throw new Error("Missing value for --format");
|
|
69
|
+
}
|
|
70
|
+
if (value !== "envelope" && value !== "handoff") {
|
|
71
|
+
throw new Error(`Unknown --format value: ${value}`);
|
|
72
|
+
}
|
|
73
|
+
format = value;
|
|
74
|
+
}
|
|
75
|
+
else if (arg.startsWith("-")) {
|
|
76
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
help,
|
|
84
|
+
subcommand,
|
|
85
|
+
runId,
|
|
86
|
+
stageId,
|
|
87
|
+
json,
|
|
88
|
+
fromPath,
|
|
89
|
+
detectStageId,
|
|
90
|
+
format,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parseSfRunDocument(raw) {
|
|
94
|
+
const trimmed = raw.trim();
|
|
95
|
+
const jsonStart = trimmed.indexOf("{");
|
|
96
|
+
const doc = jsonStart >= 0 ? trimmed.slice(jsonStart) : trimmed;
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(doc);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
throw new Error(`Failed to parse --from file JSON: ${message}`);
|
|
104
|
+
}
|
|
105
|
+
if (!parsed || typeof parsed !== "object") {
|
|
106
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
107
|
+
}
|
|
108
|
+
const record = parsed;
|
|
109
|
+
const runId = typeof record.runId === "string" && record.runId.length > 0
|
|
110
|
+
? record.runId
|
|
111
|
+
: undefined;
|
|
112
|
+
const runDir = typeof record.runDir === "string" && record.runDir.length > 0
|
|
113
|
+
? record.runDir
|
|
114
|
+
: undefined;
|
|
115
|
+
return { runId, runDir, outcome: record.outcome, ok: record.ok };
|
|
116
|
+
}
|
|
117
|
+
function assertSfRunSucceededForHandoff(doc) {
|
|
118
|
+
if (doc.outcome !== "succeeded") {
|
|
119
|
+
const got = doc.outcome === undefined || doc.outcome === null
|
|
120
|
+
? "<missing>"
|
|
121
|
+
: String(doc.outcome);
|
|
122
|
+
throw new Error(`pipeline outcome is not succeeded (got: ${got})`);
|
|
123
|
+
}
|
|
124
|
+
if (doc.ok !== true) {
|
|
125
|
+
const got = doc.ok === undefined || doc.ok === null ? "<missing>" : String(doc.ok);
|
|
126
|
+
throw new Error(`pipeline ok is not true (got: ${got})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function resolveRunContext(parsed, cwd, store) {
|
|
130
|
+
if (parsed.fromPath !== undefined) {
|
|
131
|
+
const resolved = path.resolve(cwd, parsed.fromPath);
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = readFileSync(resolved, "utf8");
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
+
throw new Error(`Failed to read --from file: ${message}`);
|
|
139
|
+
}
|
|
140
|
+
const fromDoc = parseSfRunDocument(raw);
|
|
141
|
+
if (parsed.format === "handoff") {
|
|
142
|
+
assertSfRunSucceededForHandoff(fromDoc);
|
|
143
|
+
}
|
|
144
|
+
const runId = fromDoc.runId ?? parsed.runId;
|
|
145
|
+
if (!runId) {
|
|
146
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
runId,
|
|
150
|
+
runDir: fromDoc.runDir ?? store.getWorkspaceDir(runId),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (!parsed.runId) {
|
|
154
|
+
throw new Error("Missing --run (or --from <sf-run.json>)");
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
runId: parsed.runId,
|
|
158
|
+
runDir: store.getWorkspaceDir(parsed.runId),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function isEnvelopeNotFoundError(err) {
|
|
162
|
+
return (err instanceof Error &&
|
|
163
|
+
err.message.startsWith("Envelope not found:"));
|
|
164
|
+
}
|
|
165
|
+
export const SYNTHETIC_SKIPPED_ENVELOPE = {
|
|
166
|
+
status: "skipped",
|
|
167
|
+
summary: "stage was fork-skipped",
|
|
168
|
+
artifacts: [],
|
|
169
|
+
fork_choice: null,
|
|
170
|
+
};
|
|
171
|
+
async function readStageEnvelopeOrSynthetic(store, runId, stageId) {
|
|
172
|
+
try {
|
|
173
|
+
return await store.readEnvelope(runId, stageId);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (!isEnvelopeNotFoundError(err)) {
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
const detail = await store.readRun(runId);
|
|
180
|
+
const stage = detail.stages.find((s) => s.stage_id === stageId);
|
|
181
|
+
if (!stage) {
|
|
182
|
+
throw new Error(`Stage not found: ${stageId} in run ${runId}`);
|
|
183
|
+
}
|
|
184
|
+
if (stage.status === "skipped") {
|
|
185
|
+
return SYNTHETIC_SKIPPED_ENVELOPE;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`No envelope found for stage '${stageId}' in run '${runId}'`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function formatEnvelopeHuman(stageId, envelope) {
|
|
191
|
+
return `[${stageId}] status=${envelope.status} summary: ${envelope.summary} artifacts: ${envelope.artifacts.length}`;
|
|
192
|
+
}
|
|
193
|
+
function formatHandoffHuman(result) {
|
|
194
|
+
if (result.skipped) {
|
|
195
|
+
return "handoff: skipped";
|
|
196
|
+
}
|
|
197
|
+
return `handoff: ${result.diagrams.length} diagram(s)`;
|
|
198
|
+
}
|
|
199
|
+
export async function runEnvelopeCommand(args, options = {}) {
|
|
200
|
+
const cwd = options.cwd ?? process.cwd();
|
|
201
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
202
|
+
const out = { ...defaultIo, ...options.io };
|
|
203
|
+
let parsed;
|
|
204
|
+
try {
|
|
205
|
+
parsed = parseEnvelopeArgs(args);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
209
|
+
out.error(message);
|
|
210
|
+
out.error(ENVELOPE_USAGE);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.help) {
|
|
214
|
+
out.error(ENVELOPE_USAGE);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (parsed.subcommand !== "get") {
|
|
218
|
+
out.error("Missing envelope subcommand");
|
|
219
|
+
out.error(ENVELOPE_USAGE);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
if (!parsed.stageId) {
|
|
223
|
+
out.error("Missing --stage");
|
|
224
|
+
out.error(ENVELOPE_USAGE);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const store = createRunStore({ rootDir: projectRoot });
|
|
228
|
+
let runContext;
|
|
229
|
+
try {
|
|
230
|
+
runContext = resolveRunContext(parsed, cwd, store);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
234
|
+
out.error(message);
|
|
235
|
+
out.error(ENVELOPE_USAGE);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
if (parsed.format === "handoff") {
|
|
240
|
+
if (parsed.detectStageId !== undefined) {
|
|
241
|
+
const detectEnvelope = await store.readEnvelope(runContext.runId, parsed.detectStageId);
|
|
242
|
+
if (isHandoffSkipped(detectEnvelope)) {
|
|
243
|
+
const skipped = { skipped: true };
|
|
244
|
+
if (parsed.json) {
|
|
245
|
+
out.log(JSON.stringify(skipped));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
out.log(formatHandoffHuman(skipped));
|
|
249
|
+
}
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const authorEnvelope = await readStageEnvelopeOrSynthetic(store, runContext.runId, parsed.stageId);
|
|
254
|
+
if (authorEnvelope.status === "skipped") {
|
|
255
|
+
throw new Error(`No envelope found for stage '${parsed.stageId}' in run '${runContext.runId}'`);
|
|
256
|
+
}
|
|
257
|
+
const handoff = buildHandoffDeliverable({
|
|
258
|
+
runId: runContext.runId,
|
|
259
|
+
runDir: runContext.runDir,
|
|
260
|
+
stageId: parsed.stageId,
|
|
261
|
+
envelope: authorEnvelope,
|
|
262
|
+
});
|
|
263
|
+
if (parsed.json) {
|
|
264
|
+
out.log(JSON.stringify(handoff));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
out.log(formatHandoffHuman(handoff));
|
|
268
|
+
}
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
const envelope = await readStageEnvelopeOrSynthetic(store, runContext.runId, parsed.stageId);
|
|
272
|
+
if (parsed.json) {
|
|
273
|
+
out.log(JSON.stringify(envelope));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
out.log(formatEnvelopeHuman(parsed.stageId, envelope));
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
+
out.error(message);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const EXPORT_RUN_USAGE = "Usage:\n sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]";
|
|
2
|
+
export type ExportRunCommandIo = {
|
|
3
|
+
log: (line: string) => void;
|
|
4
|
+
error: (line: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function runExportRunCommand(args: string[], options?: {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
projectRoot?: string;
|
|
9
|
+
io?: Partial<ExportRunCommandIo>;
|
|
10
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { projectRun } from "../projection/projectRun.js";
|
|
4
|
+
import { createRunStore } from "../runstore/createStore.js";
|
|
5
|
+
import { isInsideDir } from "../runstore/workspaceLayout.js";
|
|
6
|
+
export const EXPORT_RUN_USAGE = `Usage:
|
|
7
|
+
sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]`;
|
|
8
|
+
const defaultIo = {
|
|
9
|
+
log: (line) => console.log(line),
|
|
10
|
+
error: (line) => console.error(line),
|
|
11
|
+
};
|
|
12
|
+
function parseExportRunArgs(args) {
|
|
13
|
+
if (args.length === 0) {
|
|
14
|
+
return { help: false };
|
|
15
|
+
}
|
|
16
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
17
|
+
return { help: true };
|
|
18
|
+
}
|
|
19
|
+
let runId;
|
|
20
|
+
let fromPath;
|
|
21
|
+
let outPath;
|
|
22
|
+
let help = false;
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
if (arg === "--help" || arg === "-h") {
|
|
26
|
+
help = true;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === "--run") {
|
|
29
|
+
const value = args[++i];
|
|
30
|
+
if (value === undefined || value.length === 0) {
|
|
31
|
+
throw new Error("Missing value for --run");
|
|
32
|
+
}
|
|
33
|
+
runId = value;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--from") {
|
|
36
|
+
const value = args[++i];
|
|
37
|
+
if (value === undefined || value.length === 0) {
|
|
38
|
+
throw new Error("Missing value for --from");
|
|
39
|
+
}
|
|
40
|
+
fromPath = value;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === "--out") {
|
|
43
|
+
const value = args[++i];
|
|
44
|
+
if (value === undefined || value.length === 0) {
|
|
45
|
+
throw new Error("Missing value for --out");
|
|
46
|
+
}
|
|
47
|
+
outPath = value;
|
|
48
|
+
}
|
|
49
|
+
else if (arg.startsWith("-")) {
|
|
50
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { help, runId, fromPath, outPath };
|
|
57
|
+
}
|
|
58
|
+
function resolveRunIdFromFile(fromPath, cwd) {
|
|
59
|
+
const resolved = path.resolve(cwd, fromPath);
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
66
|
+
throw new Error(`Failed to read --from file: ${message}`);
|
|
67
|
+
}
|
|
68
|
+
if (!parsed || typeof parsed !== "object") {
|
|
69
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
70
|
+
}
|
|
71
|
+
const runId = parsed.runId;
|
|
72
|
+
if (typeof runId !== "string" || runId.length === 0) {
|
|
73
|
+
throw new Error("--from file must contain a JSON object with runId");
|
|
74
|
+
}
|
|
75
|
+
return runId;
|
|
76
|
+
}
|
|
77
|
+
function resolveSafeOutPath(outPath, cwd) {
|
|
78
|
+
const segments = outPath.split(/[/\\]/);
|
|
79
|
+
if (segments.some((segment) => segment === "..")) {
|
|
80
|
+
throw new Error("path must not contain .. segments");
|
|
81
|
+
}
|
|
82
|
+
const resolved = path.resolve(cwd, outPath);
|
|
83
|
+
const cwdResolved = path.resolve(cwd);
|
|
84
|
+
if (!isInsideDir(resolved, cwdResolved)) {
|
|
85
|
+
throw new Error("output path must resolve under the current working directory");
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
function assertRunComplete(status, runId) {
|
|
90
|
+
if (status === "succeeded" || status === "failed") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`run is not complete: ${runId} (status: ${status})`);
|
|
94
|
+
}
|
|
95
|
+
export async function runExportRunCommand(args, options = {}) {
|
|
96
|
+
const cwd = options.cwd ?? process.cwd();
|
|
97
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
98
|
+
const out = { ...defaultIo, ...options.io };
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseExportRunArgs(args);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
out.error(message);
|
|
106
|
+
out.error(EXPORT_RUN_USAGE);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
if (parsed.help) {
|
|
110
|
+
out.error(EXPORT_RUN_USAGE);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
let runId = parsed.runId;
|
|
114
|
+
if (parsed.fromPath !== undefined) {
|
|
115
|
+
try {
|
|
116
|
+
runId = resolveRunIdFromFile(parsed.fromPath, cwd);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
out.error(message);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!runId) {
|
|
125
|
+
out.error("Missing --run (or --from <sf-run.json>)");
|
|
126
|
+
out.error(EXPORT_RUN_USAGE);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
const store = createRunStore({ rootDir: projectRoot });
|
|
130
|
+
try {
|
|
131
|
+
const detail = await store.readRun(runId);
|
|
132
|
+
assertRunComplete(detail.status, runId);
|
|
133
|
+
const projection = projectRun(detail);
|
|
134
|
+
const json = JSON.stringify(projection, null, 2);
|
|
135
|
+
if (parsed.outPath !== undefined) {
|
|
136
|
+
const target = resolveSafeOutPath(parsed.outPath, cwd);
|
|
137
|
+
writeFileSync(target, json, "utf8");
|
|
138
|
+
out.error(`Wrote ${target}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
out.log(json);
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
147
|
+
out.error(message);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
export type HandoffDiagram = {
|
|
3
|
+
diagram_type: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
spec_path: string;
|
|
6
|
+
};
|
|
7
|
+
export type HandoffResult = {
|
|
8
|
+
skipped: true;
|
|
9
|
+
} | {
|
|
10
|
+
skipped: false;
|
|
11
|
+
runId: string;
|
|
12
|
+
runDir: string;
|
|
13
|
+
stageId: string;
|
|
14
|
+
diagrams: HandoffDiagram[];
|
|
15
|
+
};
|
|
16
|
+
export declare function isHandoffSkipped(detectEnvelope: StageEnvelope): boolean;
|
|
17
|
+
export declare function buildHandoffDeliverable(input: {
|
|
18
|
+
runId: string;
|
|
19
|
+
runDir: string;
|
|
20
|
+
stageId: string;
|
|
21
|
+
envelope: StageEnvelope;
|
|
22
|
+
}): HandoffResult;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function absArtifactPath(runDir, rel) {
|
|
2
|
+
const base = runDir.replace(/\/+$/, "");
|
|
3
|
+
const normalized = rel.replace(/^\.\//, "");
|
|
4
|
+
return `${base}/${normalized}`;
|
|
5
|
+
}
|
|
6
|
+
export function isHandoffSkipped(detectEnvelope) {
|
|
7
|
+
return (Array.isArray(detectEnvelope.fork_choice) &&
|
|
8
|
+
detectEnvelope.fork_choice.length === 0);
|
|
9
|
+
}
|
|
10
|
+
export function buildHandoffDeliverable(input) {
|
|
11
|
+
const diagrams = buildDiagrams(input.runDir, input.envelope);
|
|
12
|
+
if (diagrams.length === 0) {
|
|
13
|
+
throw new Error(`no *.spec.json artifacts or payload.diagrams in envelope for stage '${input.stageId}'`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
skipped: false,
|
|
17
|
+
runId: input.runId,
|
|
18
|
+
runDir: input.runDir,
|
|
19
|
+
stageId: input.stageId,
|
|
20
|
+
diagrams,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function buildDiagrams(runDir, envelope) {
|
|
24
|
+
const diagramsPayload = envelope.payload?.diagrams;
|
|
25
|
+
if (Array.isArray(diagramsPayload) && diagramsPayload.length > 0) {
|
|
26
|
+
return diagramsPayload.map((entry) => {
|
|
27
|
+
if (!entry || typeof entry !== "object") {
|
|
28
|
+
throw new Error("invalid diagram entry in payload.diagrams");
|
|
29
|
+
}
|
|
30
|
+
const diagram = entry;
|
|
31
|
+
const diagramType = diagram.diagram_type;
|
|
32
|
+
if (typeof diagramType !== "string" || diagramType.length === 0) {
|
|
33
|
+
throw new Error("payload.diagrams entry missing diagram_type");
|
|
34
|
+
}
|
|
35
|
+
const suffix = `${diagramType}.spec.json`;
|
|
36
|
+
const rel = envelope.artifacts.find((artifact) => artifact.endsWith(suffix));
|
|
37
|
+
if (!rel) {
|
|
38
|
+
throw new Error(`no artifact matching ${diagramType}.spec.json in envelope for stage`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
diagram_type: diagramType,
|
|
42
|
+
summary: typeof diagram.summary === "string" ? diagram.summary : "",
|
|
43
|
+
spec_path: absArtifactPath(runDir, rel),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return envelope.artifacts
|
|
48
|
+
.filter((artifact) => /\.spec\.json$/.test(artifact))
|
|
49
|
+
.map((rel) => {
|
|
50
|
+
const match = rel.match(/([^/]+)\.spec\.json$/);
|
|
51
|
+
const diagramType = match?.[1];
|
|
52
|
+
if (!diagramType) {
|
|
53
|
+
throw new Error(`invalid spec artifact path: ${rel}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
diagram_type: diagramType,
|
|
57
|
+
summary: envelope.summary ?? "",
|
|
58
|
+
spec_path: absArtifactPath(runDir, rel),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/cli/runCommand.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
1
2
|
import { type StartRunResult } from "../runtime/runManager.js";
|
|
2
3
|
import { type CliRunReportIo } from "./runOutput.js";
|
|
3
|
-
export declare const RUN_USAGE = "Usage:\n sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]";
|
|
4
|
+
export declare const RUN_USAGE = "Usage:\n sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]";
|
|
4
5
|
export type RunCommandIo = CliRunReportIo;
|
|
5
6
|
export type StartRunFn = (input: {
|
|
6
7
|
task: string;
|
|
@@ -15,6 +16,8 @@ export declare function completeCliRun(started: Extract<StartRunResult, {
|
|
|
15
16
|
ok: true;
|
|
16
17
|
}>, io?: RunCommandIo, options?: {
|
|
17
18
|
json?: boolean;
|
|
19
|
+
store?: RunStore;
|
|
20
|
+
includeStages?: boolean;
|
|
18
21
|
}): Promise<number>;
|
|
19
22
|
export declare function runRunCommand(args: string[], options?: {
|
|
20
23
|
cwd?: string;
|
|
@@ -22,5 +25,6 @@ export declare function runRunCommand(args: string[], options?: {
|
|
|
22
25
|
isGitProject?: boolean;
|
|
23
26
|
io?: Partial<RunCommandIo>;
|
|
24
27
|
startRun?: StartRunFn;
|
|
28
|
+
store?: RunStore;
|
|
25
29
|
env?: Record<string, string | undefined>;
|
|
26
30
|
}): Promise<number>;
|
package/dist/cli/runCommand.js
CHANGED
|
@@ -8,17 +8,17 @@ import { resolveCiIdentity } from "./ciIdentity.js";
|
|
|
8
8
|
import { resolveOperatorCatalog } from "./operatorCatalog.js";
|
|
9
9
|
import { exitCodeForValidation, formatValidationHuman, formatValidationJson, } from "./validateOutput.js";
|
|
10
10
|
export const RUN_USAGE = `Usage:
|
|
11
|
-
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]`;
|
|
11
|
+
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]`;
|
|
12
12
|
const defaultIo = {
|
|
13
13
|
log: (line) => console.log(line),
|
|
14
14
|
error: (line) => console.error(line),
|
|
15
15
|
};
|
|
16
16
|
function parseRunArgs(args) {
|
|
17
17
|
if (args.length === 0) {
|
|
18
|
-
return { help: false, json: false, skipGates: false };
|
|
18
|
+
return { help: false, json: false, includeStages: false, skipGates: false };
|
|
19
19
|
}
|
|
20
20
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
21
|
-
return { help: true, json: false, skipGates: false };
|
|
21
|
+
return { help: true, json: false, includeStages: false, skipGates: false };
|
|
22
22
|
}
|
|
23
23
|
let task;
|
|
24
24
|
let pipeline;
|
|
@@ -29,6 +29,7 @@ function parseRunArgs(args) {
|
|
|
29
29
|
let operatorCwd;
|
|
30
30
|
let operatorAgentDir;
|
|
31
31
|
let json = false;
|
|
32
|
+
let includeStages = false;
|
|
32
33
|
let skipGates = false;
|
|
33
34
|
let help = false;
|
|
34
35
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -95,6 +96,18 @@ function parseRunArgs(args) {
|
|
|
95
96
|
else if (arg === "--json") {
|
|
96
97
|
json = true;
|
|
97
98
|
}
|
|
99
|
+
else if (arg === "--include") {
|
|
100
|
+
const value = args[++i];
|
|
101
|
+
if (value === undefined || value.length === 0) {
|
|
102
|
+
throw new Error("Missing value for --include");
|
|
103
|
+
}
|
|
104
|
+
if (value === "stages") {
|
|
105
|
+
includeStages = true;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new Error(`Unknown --include value: ${value}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
98
111
|
else if (arg === "--skip-gates") {
|
|
99
112
|
skipGates = true;
|
|
100
113
|
}
|
|
@@ -108,6 +121,7 @@ function parseRunArgs(args) {
|
|
|
108
121
|
return {
|
|
109
122
|
help,
|
|
110
123
|
json,
|
|
124
|
+
includeStages,
|
|
111
125
|
skipGates,
|
|
112
126
|
task,
|
|
113
127
|
pipeline,
|
|
@@ -136,7 +150,12 @@ function defaultStartRun(cwd, projectRoot, isGitProject, operatorCatalog) {
|
|
|
136
150
|
}
|
|
137
151
|
export async function completeCliRun(started, io = defaultIo, options = {}) {
|
|
138
152
|
const result = await started.done;
|
|
139
|
-
return reportCliRun({ kind: "completion", result }, {
|
|
153
|
+
return reportCliRun({ kind: "completion", result }, {
|
|
154
|
+
json: options.json,
|
|
155
|
+
io,
|
|
156
|
+
store: options.store,
|
|
157
|
+
includeStages: options.includeStages,
|
|
158
|
+
});
|
|
140
159
|
}
|
|
141
160
|
export async function runRunCommand(args, options = {}) {
|
|
142
161
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -162,6 +181,10 @@ export async function runRunCommand(args, options = {}) {
|
|
|
162
181
|
out.error(RUN_USAGE);
|
|
163
182
|
return 1;
|
|
164
183
|
}
|
|
184
|
+
if (parsed.includeStages && !parsed.json) {
|
|
185
|
+
out.error("error: --include stages requires --json");
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
165
188
|
const operatorCatalog = resolveOperatorCatalog({
|
|
166
189
|
flags: {
|
|
167
190
|
operatorCwd: parsed.operatorCwd,
|
|
@@ -180,6 +203,10 @@ export async function runRunCommand(args, options = {}) {
|
|
|
180
203
|
},
|
|
181
204
|
env: options.env ?? process.env,
|
|
182
205
|
});
|
|
206
|
+
const store = options.store ??
|
|
207
|
+
(parsed.includeStages
|
|
208
|
+
? createRunStore({ rootDir: projectRoot })
|
|
209
|
+
: undefined);
|
|
183
210
|
try {
|
|
184
211
|
const started = await startRun({
|
|
185
212
|
task: parsed.task,
|
|
@@ -191,7 +218,11 @@ export async function runRunCommand(args, options = {}) {
|
|
|
191
218
|
if (!started.ok) {
|
|
192
219
|
return reportCliRun({ kind: "start-failure", started }, { json: parsed.json, io: out });
|
|
193
220
|
}
|
|
194
|
-
return completeCliRun(started, out, {
|
|
221
|
+
return completeCliRun(started, out, {
|
|
222
|
+
json: parsed.json,
|
|
223
|
+
includeStages: parsed.includeStages,
|
|
224
|
+
store,
|
|
225
|
+
});
|
|
195
226
|
}
|
|
196
227
|
catch (err) {
|
|
197
228
|
if (err instanceof PipelineValidationError) {
|
package/dist/cli/runOutput.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
1
2
|
import type { PipelineRunResult } from "../runtime/pipelineRunner.js";
|
|
2
3
|
import type { StartRunResult } from "../runtime/runManager.js";
|
|
3
4
|
export type CliRunReportIo = {
|
|
@@ -16,4 +17,6 @@ export type CliRunReportEvent = {
|
|
|
16
17
|
export declare function reportCliRun(event: CliRunReportEvent, options: {
|
|
17
18
|
json?: boolean;
|
|
18
19
|
io: CliRunReportIo;
|
|
19
|
-
|
|
20
|
+
store?: RunStore;
|
|
21
|
+
includeStages?: boolean;
|
|
22
|
+
}): Promise<number>;
|