stageflow 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -0,0 +1,120 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { readRunArtifact } from "../mcp/readArtifact.js";
4
+ import { createRunStore } from "../runstore/createStore.js";
5
+ import { isInsideDir } from "../runstore/workspaceLayout.js";
6
+ export const ARTIFACT_USAGE = `Usage:
7
+ sf artifact read --run <runId> --path <relPath> [--out <file>]`;
8
+ const defaultIo = {
9
+ log: (line) => console.log(line),
10
+ error: (line) => console.error(line),
11
+ };
12
+ function parseArtifactArgs(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
+ const subcommand = args[0];
20
+ if (subcommand !== "read") {
21
+ throw new Error(`Unknown artifact subcommand: ${subcommand}`);
22
+ }
23
+ let runId;
24
+ let artifactPath;
25
+ let outPath;
26
+ let help = false;
27
+ for (let i = 1; i < args.length; i++) {
28
+ const arg = args[i];
29
+ if (arg === "--help" || arg === "-h") {
30
+ help = true;
31
+ }
32
+ else if (arg === "--run") {
33
+ const value = args[++i];
34
+ if (value === undefined || value.length === 0) {
35
+ throw new Error("Missing value for --run");
36
+ }
37
+ runId = value;
38
+ }
39
+ else if (arg === "--path") {
40
+ const value = args[++i];
41
+ if (value === undefined || value.length === 0) {
42
+ throw new Error("Missing value for --path");
43
+ }
44
+ artifactPath = value;
45
+ }
46
+ else if (arg === "--out") {
47
+ const value = args[++i];
48
+ if (value === undefined || value.length === 0) {
49
+ throw new Error("Missing value for --out");
50
+ }
51
+ outPath = value;
52
+ }
53
+ else if (arg.startsWith("-")) {
54
+ throw new Error(`Unknown flag: ${arg}`);
55
+ }
56
+ else {
57
+ throw new Error(`Unexpected argument: ${arg}`);
58
+ }
59
+ }
60
+ return { help, subcommand, runId, artifactPath, outPath };
61
+ }
62
+ function resolveSafeOutPath(outPath, cwd) {
63
+ const segments = outPath.split(/[/\\]/);
64
+ if (segments.some((segment) => segment === "..")) {
65
+ throw new Error("path must not contain .. segments");
66
+ }
67
+ const resolved = path.resolve(cwd, outPath);
68
+ const cwdResolved = path.resolve(cwd);
69
+ if (!isInsideDir(resolved, cwdResolved)) {
70
+ throw new Error("output path must resolve under the current working directory");
71
+ }
72
+ return resolved;
73
+ }
74
+ export async function runArtifactCommand(args, options = {}) {
75
+ const cwd = options.cwd ?? process.cwd();
76
+ const projectRoot = options.projectRoot ?? cwd;
77
+ const out = { ...defaultIo, ...options.io };
78
+ let parsed;
79
+ try {
80
+ parsed = parseArtifactArgs(args);
81
+ }
82
+ catch (err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ out.error(message);
85
+ out.error(ARTIFACT_USAGE);
86
+ return 1;
87
+ }
88
+ if (parsed.help) {
89
+ out.error(ARTIFACT_USAGE);
90
+ return 0;
91
+ }
92
+ if (parsed.subcommand !== "read") {
93
+ out.error("Missing artifact subcommand");
94
+ out.error(ARTIFACT_USAGE);
95
+ return 1;
96
+ }
97
+ if (!parsed.runId || !parsed.artifactPath) {
98
+ out.error("Missing --run and/or --path");
99
+ out.error(ARTIFACT_USAGE);
100
+ return 1;
101
+ }
102
+ const store = createRunStore({ rootDir: projectRoot });
103
+ try {
104
+ const contents = await readRunArtifact(store, parsed.runId, parsed.artifactPath);
105
+ if (parsed.outPath !== undefined) {
106
+ const target = resolveSafeOutPath(parsed.outPath, cwd);
107
+ writeFileSync(target, contents, "utf8");
108
+ out.error(`Wrote ${target}`);
109
+ }
110
+ else {
111
+ out.log(contents);
112
+ }
113
+ return 0;
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ out.error(message);
118
+ return 1;
119
+ }
120
+ }
@@ -0,0 +1,32 @@
1
+ import type { StageEnvelope } from "../types/envelope.js";
2
+ export declare const ENVELOPE_USAGE = "Usage:\n sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]";
3
+ export type EnvelopeCommandIo = {
4
+ log: (line: string) => void;
5
+ error: (line: string) => void;
6
+ };
7
+ export type EnvelopeOutput = StageEnvelope | SyntheticSkippedEnvelope;
8
+ export type SyntheticSkippedEnvelope = {
9
+ status: "skipped";
10
+ summary: "stage was fork-skipped";
11
+ artifacts: [];
12
+ fork_choice: null;
13
+ };
14
+ type EnvelopeFormat = "envelope" | "handoff";
15
+ type ParsedEnvelopeArgs = {
16
+ help: boolean;
17
+ subcommand?: string;
18
+ runId?: string;
19
+ stageId?: string;
20
+ json: boolean;
21
+ fromPath?: string;
22
+ detectStageId?: string;
23
+ format: EnvelopeFormat;
24
+ };
25
+ export declare function parseEnvelopeArgs(args: string[]): ParsedEnvelopeArgs;
26
+ export declare const SYNTHETIC_SKIPPED_ENVELOPE: SyntheticSkippedEnvelope;
27
+ export declare function runEnvelopeCommand(args: string[], options?: {
28
+ cwd?: string;
29
+ projectRoot?: string;
30
+ io?: Partial<EnvelopeCommandIo>;
31
+ }): Promise<number>;
32
+ export {};
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export declare const INIT_USAGE = "Usage:\n sf init";
2
+ export type InitCommandIo = {
3
+ log: (line: string) => void;
4
+ error: (line: string) => void;
5
+ };
6
+ export declare function runInitCommand(args: string[], options?: {
7
+ cwd?: string;
8
+ io?: Partial<InitCommandIo>;
9
+ }): Promise<number>;