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.
Files changed (153) hide show
  1. package/README.md +22 -1
  2. package/dist/agent/activity.d.ts +2 -0
  3. package/dist/agent/cursorProvider.d.ts +1 -10
  4. package/dist/agent/cursorProvider.js +18 -10
  5. package/dist/agent/fakeAgent.js +11 -7
  6. package/dist/agent/piAdapter.d.ts +2 -1
  7. package/dist/agent/piAdapter.js +28 -25
  8. package/dist/agent/port.d.ts +6 -1
  9. package/dist/agent/port.js +3 -0
  10. package/dist/cli/artifactCommand.d.ts +10 -0
  11. package/dist/cli/artifactCommand.js +120 -0
  12. package/dist/cli/envelopeCommand.d.ts +32 -0
  13. package/dist/cli/envelopeCommand.js +285 -0
  14. package/dist/cli/exportRunCommand.d.ts +10 -0
  15. package/dist/cli/exportRunCommand.js +150 -0
  16. package/dist/cli/handoffFormat.d.ts +22 -0
  17. package/dist/cli/handoffFormat.js +61 -0
  18. package/dist/cli/runCommand.d.ts +5 -1
  19. package/dist/cli/runCommand.js +36 -5
  20. package/dist/cli/runOutput.d.ts +4 -1
  21. package/dist/cli/runOutput.js +36 -8
  22. package/dist/cli/skillsCommand.d.ts +16 -0
  23. package/dist/cli/skillsCommand.js +459 -0
  24. package/dist/cli.js +78 -5
  25. package/dist/config/loadPipeline.js +5 -1
  26. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  27. package/dist/config/normalizePipelineStageEntry.js +25 -1
  28. package/dist/config/pipelineStageKeys.js +1 -1
  29. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  30. package/dist/config/resolveForkEmitContext.js +23 -1
  31. package/dist/config/resolvePipelineDag.js +36 -4
  32. package/dist/envelope/check.d.ts +2 -0
  33. package/dist/envelope/check.js +51 -0
  34. package/dist/envelope/cloneForks.d.ts +2 -0
  35. package/dist/envelope/cloneForks.js +62 -0
  36. package/dist/envelope/forkChoice.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +2 -0
  39. package/dist/mcp/catalogTools.d.ts +3 -0
  40. package/dist/mcp/catalogTools.js +187 -0
  41. package/dist/mcp/controlTools.d.ts +3 -0
  42. package/dist/mcp/controlTools.js +147 -0
  43. package/dist/mcp/deps.d.ts +11 -0
  44. package/dist/mcp/deps.js +1 -0
  45. package/dist/mcp/projectRun.d.ts +1 -40
  46. package/dist/mcp/projectRun.js +1 -64
  47. package/dist/mcp/resources.d.ts +5 -0
  48. package/dist/mcp/resources.js +45 -0
  49. package/dist/mcp/server.d.ts +16 -3
  50. package/dist/mcp/server.js +159 -3
  51. package/dist/mcp/toolResults.d.ts +7 -0
  52. package/dist/mcp/toolResults.js +6 -0
  53. package/dist/mcp/tools.d.ts +2 -7
  54. package/dist/mcp/tools.js +52 -103
  55. package/dist/mcp/waitRun.d.ts +46 -0
  56. package/dist/mcp/waitRun.js +146 -0
  57. package/dist/mcp/waitingGates.d.ts +12 -0
  58. package/dist/mcp/waitingGates.js +83 -0
  59. package/dist/projection/projectRun.d.ts +44 -0
  60. package/dist/projection/projectRun.js +71 -0
  61. package/dist/prompt/priorEnvelope.d.ts +1 -1
  62. package/dist/prompt/priorEnvelope.js +4 -1
  63. package/dist/runstore/paths.js +2 -0
  64. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  65. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  66. package/dist/runstore/port.d.ts +13 -2
  67. package/dist/runstore/port.js +4 -1
  68. package/dist/runstore/runProjection.d.ts +1 -1
  69. package/dist/runstore/runProjection.js +14 -3
  70. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  71. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  72. package/dist/runstore/stageInstanceId.d.ts +4 -0
  73. package/dist/runstore/stageInstanceId.js +23 -0
  74. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  75. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  76. package/dist/runstore/trackProjection.js +28 -12
  77. package/dist/runstore/workspaceLayout.d.ts +1 -0
  78. package/dist/runstore/workspaceLayout.js +1 -1
  79. package/dist/runtime/cloneSchedule.d.ts +18 -0
  80. package/dist/runtime/cloneSchedule.js +233 -0
  81. package/dist/runtime/envelopeRouting.d.ts +2 -1
  82. package/dist/runtime/envelopeRouting.js +80 -13
  83. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  84. package/dist/runtime/pipelineScheduler.js +166 -30
  85. package/dist/runtime/resumeReconstruct.js +8 -3
  86. package/dist/runtime/runChangeBus.d.ts +18 -0
  87. package/dist/runtime/runChangeBus.js +83 -0
  88. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  89. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  90. package/dist/runtime/stageRunner.d.ts +1 -0
  91. package/dist/runtime/stageRunner.js +17 -15
  92. package/dist/runtime/stageWorker.js +11 -4
  93. package/dist/server/bootstrap.d.ts +32 -0
  94. package/dist/server/bootstrap.js +56 -0
  95. package/dist/server/createHttpHost.d.ts +34 -0
  96. package/dist/server/createHttpHost.js +70 -0
  97. package/dist/server/http.d.ts +6 -13
  98. package/dist/server/http.js +295 -374
  99. package/dist/server/mcpHost.d.ts +22 -0
  100. package/dist/server/mcpHost.js +21 -0
  101. package/dist/server/operatorResults.d.ts +13 -0
  102. package/dist/server/operatorResults.js +22 -0
  103. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  104. package/dist/tools/emitStageEnvelope.js +22 -6
  105. package/dist/types/envelope.d.ts +2 -0
  106. package/dist/types/forkChoice.d.ts +22 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  109. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +2 -1
  112. package/skills/install-suite.sh +107 -0
  113. package/skills/stageflow/SKILL.md +26 -0
  114. package/skills/stageflow/references/control-surface.md +26 -0
  115. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  116. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  117. package/skills/stageflow-author/SKILL.md +58 -0
  118. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  119. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  120. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  121. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  122. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  123. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  124. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  125. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  126. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  127. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  128. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  130. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  131. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  132. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  133. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  134. package/skills/stageflow-delegate/SKILL.md +18 -0
  135. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  136. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  137. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  138. package/skills/stageflow-run/SKILL.md +151 -0
  139. package/skills/stageflow-run/references/mcp-call.md +17 -0
  140. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  141. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  142. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  143. package/skills/stageflow-session-capture/SKILL.md +94 -0
  144. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  145. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  146. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  147. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  148. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  149. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  150. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  151. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  152. package/skills/stageflow-setup/SKILL.md +110 -0
  153. package/dist/ui/assets/index-CFSzDZje.js +0 -118
package/README.md CHANGED
@@ -36,6 +36,7 @@ The same pipeline runs three ways without rewriting anything:
36
36
  - **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` is running
37
37
  - **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
38
38
  - **Parallel stages** — pipeline DAG with fan-out and join (see [YAML catalog](docs/yaml-catalog.md))
39
+ - **Clonable fan-out** — clone one successor N times at completion, then join (see [YAML catalog](docs/yaml-catalog.md#clonable-successors))
39
40
  - **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
40
41
 
41
42
  ## Installation
@@ -60,6 +61,14 @@ npm i -g ./stageflow-*.tgz
60
61
 
61
62
  `better-sqlite3` ships prebuilds for common platforms. `--ignore-scripts` is fine when a prebuild exists. Benign `node-gyp` warnings during install can be ignored if `require("better-sqlite3")` works.
62
63
 
64
+ **Harness skills** (Cursor, Claude Code, Codex, Pi, OpenCode) — from a consumer project:
65
+
66
+ ```bash
67
+ npx skills add tejasghutukade/stageflow
68
+ ```
69
+
70
+ Then ask the agent to set up Stageflow. Details: [docs/skills-suite.md](docs/skills-suite.md).
71
+
63
72
  ## Quick start
64
73
 
65
74
  In a project directory (preferably a git repo):
@@ -155,6 +164,16 @@ The process exits `0` when the Run succeeded, `1` when it failed (including a bu
155
164
 
156
165
  On a mixed Pipeline, default wait parks the Run (exit `2`). `--skip-gates` fails the Stage (exit `1`). A Pipeline with no HITL does not need the flag.
157
166
 
167
+ Post-run extraction (dogfooded in [Archify PR diagrams](examples/archify-on-pr/)):
168
+
169
+ ```bash
170
+ sf run ... --json --include stages > sf-run.json
171
+ sf envelope get --from sf-run.json --stage author-diagrams --format handoff --json
172
+ sf skills install --from-zip <url> --skill-name archify
173
+ ```
174
+
175
+ See [docs/ci.md](docs/ci.md) for the full CI recipe and [`.github/actions/sf-run`](.github/actions/sf-run) composite action.
176
+
158
177
  ## State
159
178
 
160
179
  Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository (SQLite + per-run workspaces under `.stageflow/runs/`). Global config and `sf_owned` auth live under **`~/.stageflow/`**. If `.stageflow` is missing and `.software-factory` exists from an older install, the next store open renames it to `.stageflow` once.
@@ -187,8 +206,10 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
187
206
 
188
207
  | Example | Description |
189
208
  |---------|-------------|
209
+ | **[archify-on-pr](examples/archify-on-pr/)** | **Featured** — PR diagram automation: conditional fork, skill binding, envelope handoff, GHA deliver |
190
210
  | [hello-world](examples/hello-world/) | Single stage, domain-neutral |
191
211
  | [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
212
+ | [conditional-fork](examples/conditional-fork/) | Exclusive fork routing with operator branch choice |
192
213
  | [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
193
214
  | [ci-validate](examples/ci-validate/) | Strict validate in CI |
194
215
 
@@ -203,7 +224,7 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
203
224
  | [docs/README.md](docs/README.md) | Documentation index |
204
225
  | [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
205
226
  | [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
206
- | [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf validate`, `sf ui`, `sf providers` |
227
+ | [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf envelope`, `sf skills`, `sf validate`, `sf ui`, `sf providers` |
207
228
  | [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
208
229
  | [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
209
230
  | [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
@@ -68,6 +68,8 @@ export type StageLifecycleEvent = {
68
68
  } | {
69
69
  event: "failed";
70
70
  reason: string;
71
+ } | {
72
+ event: "skipped";
71
73
  };
72
74
  export type StageLogLine = StageActivityEvent | StageLifecycleEvent;
73
75
  export declare function truncateActivityText(value: unknown, limit?: number): string | undefined;
@@ -1,14 +1,5 @@
1
1
  import { type StageProviderSupport } from "./providerSupport.js";
2
- /**
3
- * Locate the pi-cursor-sdk extension entry without opening full global package
4
- * discovery. Stages stay sealed; only this allowlisted path is loaded.
5
- *
6
- * Resolution order:
7
- * 1. STAGEFLOW_CURSOR_EXTENSION (absolute path to the extension .ts/.js)
8
- * 2. Path package from ~/.pi/agent/settings.json (same source interactive pi uses)
9
- * 3. npm install under ~/.pi/agent/npm/node_modules/pi-cursor-sdk
10
- * 4. Sibling checkout at ../pi-cursor-sdk relative to this repo
11
- */
2
+ export declare function cursorExtensionEntryInPackage(packageRoot: string): string | undefined;
12
3
  export declare function resolveCursorExtensionPath(): string | undefined;
13
4
  export declare function isCursorModelRef(modelRef: string): boolean;
14
5
  export declare const cursorProviderSupport: StageProviderSupport;
@@ -18,8 +18,19 @@ const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
18
18
  * 1. STAGEFLOW_CURSOR_EXTENSION (absolute path to the extension .ts/.js)
19
19
  * 2. Path package from ~/.pi/agent/settings.json (same source interactive pi uses)
20
20
  * 3. npm install under ~/.pi/agent/npm/node_modules/pi-cursor-sdk
21
+ * (`dist/index.js` for 0.3+, `src/index.ts` for older publishes)
21
22
  * 4. Sibling checkout at ../pi-cursor-sdk relative to this repo
22
23
  */
24
+ const CURSOR_PACKAGE_ENTRIES = ["dist/index.js", "src/index.ts"];
25
+ export function cursorExtensionEntryInPackage(packageRoot) {
26
+ for (const rel of CURSOR_PACKAGE_ENTRIES) {
27
+ const full = path.join(packageRoot, rel);
28
+ if (existsSync(full)) {
29
+ return full;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
23
34
  export function resolveCursorExtensionPath() {
24
35
  const fromEnv = process.env.STAGEFLOW_CURSOR_EXTENSION?.trim();
25
36
  if (fromEnv && existsSync(fromEnv)) {
@@ -29,16 +40,13 @@ export function resolveCursorExtensionPath() {
29
40
  if (fromSettings) {
30
41
  return fromSettings;
31
42
  }
32
- const npmEntry = path.join(os.homedir(), ".pi", "agent", "npm", "node_modules", "pi-cursor-sdk", "src", "index.ts");
33
- if (existsSync(npmEntry)) {
43
+ const npmEntry = cursorExtensionEntryInPackage(path.join(os.homedir(), ".pi", "agent", "npm", "node_modules", "pi-cursor-sdk"));
44
+ if (npmEntry) {
34
45
  return npmEntry;
35
46
  }
36
47
  const here = path.dirname(fileURLToPath(import.meta.url));
37
- const sibling = path.resolve(here, "../../../pi-cursor-sdk/src/index.ts");
38
- if (existsSync(sibling)) {
39
- return sibling;
40
- }
41
- return undefined;
48
+ const siblingRoot = path.resolve(here, "../../../pi-cursor-sdk");
49
+ return cursorExtensionEntryInPackage(siblingRoot);
42
50
  }
43
51
  export function isCursorModelRef(modelRef) {
44
52
  const slash = modelRef.indexOf("/");
@@ -60,7 +68,7 @@ function missingExtensionReason(modelRef) {
60
68
  return [
61
69
  `Model "${modelRef}" requires pi-cursor-sdk, but no extension entry was found.`,
62
70
  "Install with `pi install npm:pi-cursor-sdk`, or set STAGEFLOW_CURSOR_EXTENSION",
63
- "to the absolute path of pi-cursor-sdk/src/index.ts.",
71
+ "to the absolute path of pi-cursor-sdk/dist/index.js (or src/index.ts).",
64
72
  "Also ensure a Cursor SDK API key is available via Pi /login or CURSOR_API_KEY.",
65
73
  ].join(" ");
66
74
  }
@@ -130,8 +138,8 @@ function resolveFromPiSettings() {
130
138
  }
131
139
  }
132
140
  }
133
- const declared = path.join(packageRoot, "src", "index.ts");
134
- if (existsSync(declared)) {
141
+ const declared = cursorExtensionEntryInPackage(packageRoot);
142
+ if (declared) {
135
143
  return declared;
136
144
  }
137
145
  }
@@ -2,10 +2,11 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
2
2
  import path from "node:path";
3
3
  import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
4
4
  import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
5
+ import { assertCloneForks } from "../envelope/cloneForks.js";
5
6
  import { assertForkEnvelope } from "../envelope/forkChoice.js";
6
7
  import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
7
8
  import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
8
- import { runStageViaOpen } from "./port.js";
9
+ import { runtimeStageId, runStageViaOpen } from "./port.js";
9
10
  function opaqueEqual(a, b) {
10
11
  if (Object.is(a, b))
11
12
  return true;
@@ -83,7 +84,7 @@ export class FakeAgent {
83
84
  return answer;
84
85
  };
85
86
  if (behavior.type === "wait_then_emit") {
86
- const loaded = loadFakeHitlResume(input.roots, input.stage.id);
87
+ const loaded = loadFakeHitlResume(input.roots, runtimeStageId(input));
87
88
  if (loaded === "corrupt") {
88
89
  resumeCorrupt = true;
89
90
  }
@@ -116,7 +117,7 @@ export class FakeAgent {
116
117
  });
117
118
  };
118
119
  const finishEmit = () => {
119
- clearFakeHitlResume(input.roots, input.stage.id);
120
+ clearFakeHitlResume(input.roots, runtimeStageId(input));
120
121
  if (behavior.type === "throw") {
121
122
  input.onActivity?.({ event: "agent_end" });
122
123
  return {
@@ -139,6 +140,9 @@ export class FakeAgent {
139
140
  if (input.forkEmitContext !== undefined) {
140
141
  assertForkEnvelope(envelope, input.forkEmitContext);
141
142
  }
143
+ if (input.cloneEmitContext !== undefined) {
144
+ assertCloneForks(envelope, input.cloneEmitContext);
145
+ }
142
146
  assertEnvelopePayload(envelope, input.stage.payload_schema);
143
147
  if (!isAdvancingEnvelope(envelope)) {
144
148
  input.onActivity?.({ event: "agent_end" });
@@ -171,7 +175,7 @@ export class FakeAgent {
171
175
  }
172
176
  };
173
177
  return {
174
- stageId: input.stage.id,
178
+ stageId: runtimeStageId(input),
175
179
  async next() {
176
180
  if (closed) {
177
181
  return {
@@ -210,7 +214,7 @@ export class FakeAgent {
210
214
  }
211
215
  catch (err) {
212
216
  input.onActivity?.({ event: "agent_end" });
213
- clearFakeHitlResume(input.roots, input.stage.id);
217
+ clearFakeHitlResume(input.roots, runtimeStageId(input));
214
218
  return {
215
219
  status: "completed",
216
220
  result: {
@@ -227,7 +231,7 @@ export class FakeAgent {
227
231
  answer !== undefined &&
228
232
  !opaqueEqual(answer, expected)) {
229
233
  input.onActivity?.({ event: "agent_end" });
230
- clearFakeHitlResume(input.roots, input.stage.id);
234
+ clearFakeHitlResume(input.roots, runtimeStageId(input));
231
235
  return {
232
236
  status: "completed",
233
237
  result: {
@@ -241,7 +245,7 @@ export class FakeAgent {
241
245
  const request = behavior.waitRequests[waitIndex];
242
246
  waitIndex += 1;
243
247
  armWait();
244
- writeFakeHitlResume(input.roots, input.stage.id, {
248
+ writeFakeHitlResume(input.roots, runtimeStageId(input), {
245
249
  waitRequests: behavior.waitRequests,
246
250
  expectedAnswers: behavior.expectedAnswers,
247
251
  envelope: behavior.envelope,
@@ -84,6 +84,7 @@ export declare function createSealedResourceLoader(options: {
84
84
  }): DefaultResourceLoader;
85
85
  /** Stable Pi session JSONL path under the run workspace stage dir. */
86
86
  export declare function stageSessionFilePath(roots: Pick<StageRoots, "runWorkspaceDir" | "attempt">, stageId: string): string;
87
+ export declare function resolveStageSessionFile(input: StageRunInput): string;
87
88
  export declare class StageSessionReconstructError extends Error {
88
89
  readonly stageId: string;
89
90
  readonly sessionFile: string;
@@ -97,7 +98,7 @@ export declare class StageSessionReconstructError extends Error {
97
98
  * Create or reopen the durable stage session file under the run workspace.
98
99
  * Touches an empty file first so Pi flushes the session header immediately.
99
100
  */
100
- export declare function createStageSessionManager(roots: StageRoots, stageId: string): Promise<SessionManager>;
101
+ export declare function createStageSessionManager(roots: StageRoots, stageId: string, sessionFile?: string): Promise<SessionManager>;
101
102
  /**
102
103
  * Open an existing stage session for post-restart reconstruct.
103
104
  * Missing or corrupt files fail closed (KTD7) — never create a fresh session.
@@ -43,7 +43,7 @@ import "./cursorProvider.js";
43
43
  import { findProviderSupport } from "./providerSupport.js";
44
44
  import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
45
45
  import { createStageActivityObserver, } from "./activityObserver.js";
46
- import { DEFAULT_STAGE_TIMEOUT_MS, runStageViaOpen } from "./port.js";
46
+ import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
47
47
  /**
48
48
  * Stage tool allowlist for sealed Pi sessions.
49
49
  * Always includes `ask_operator` (R7); `write_stage_artifact` when registered
@@ -365,7 +365,7 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
365
365
  emitHint += `\nOn status=success, payload is required and must match this JSON Schema:\n${JSON.stringify(input.stage.payload_schema, null, 2)}`;
366
366
  }
367
367
  const attempt = input.roots.attempt ?? 1;
368
- const attemptArtifactsPath = `stages/${input.stage.id}/attempts/${attempt}/artifacts/`;
368
+ const attemptArtifactsPath = `stages/${runtimeStageId(input)}/attempts/${attempt}/artifacts/`;
369
369
  const skillBaseDir = input.stage.skill !== undefined && input.skillFilePath !== undefined
370
370
  ? path.dirname(input.skillFilePath)
371
371
  : undefined;
@@ -390,11 +390,11 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
390
390
  : `Create factory stage artifacts under ${attemptArtifactsPath} relative to the run folder.`;
391
391
  return [
392
392
  `Task id: ${input.task.id}`,
393
- `Stage id: ${input.stage.id}`,
393
+ `Stage id: ${runtimeStageId(input)}`,
394
394
  `Goal: ${input.task.goal}`,
395
395
  input.task.context ? `Context: ${input.task.context}` : "",
396
396
  input.task.constraints ? `Constraints: ${input.task.constraints}` : "",
397
- formatPriorEnvelope(input.priorEnvelope),
397
+ formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes),
398
398
  "",
399
399
  artifactGuidance,
400
400
  emitHint,
@@ -490,6 +490,10 @@ export function stageSessionFilePath(roots, stageId) {
490
490
  : noAttemptContext();
491
491
  return ctx.sessionPath(roots.runWorkspaceDir, stageId);
492
492
  }
493
+ export function resolveStageSessionFile(input) {
494
+ return (input.resumeToken ??
495
+ stageSessionFilePath(input.roots, runtimeStageId(input)));
496
+ }
493
497
  export class StageSessionReconstructError extends Error {
494
498
  stageId;
495
499
  sessionFile;
@@ -504,8 +508,7 @@ export class StageSessionReconstructError extends Error {
504
508
  * Create or reopen the durable stage session file under the run workspace.
505
509
  * Touches an empty file first so Pi flushes the session header immediately.
506
510
  */
507
- export async function createStageSessionManager(roots, stageId) {
508
- const sessionFile = stageSessionFilePath(roots, stageId);
511
+ export async function createStageSessionManager(roots, stageId, sessionFile = stageSessionFilePath(roots, stageId)) {
509
512
  await mkdir(path.dirname(sessionFile), { recursive: true });
510
513
  if (!(await fileExists(sessionFile))) {
511
514
  await writeFile(sessionFile, "");
@@ -683,7 +686,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
683
686
  const provider = findProviderSupport(input.stage.model);
684
687
  const capture = {};
685
688
  const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
686
- const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext);
689
+ const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext);
687
690
  const emitTool = defineTool(emitDef);
688
691
  const askDef = createAskOperatorTool({
689
692
  requestWait: (prompt) => askWaitChannel.requestWait(prompt),
@@ -691,7 +694,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
691
694
  const askTool = defineTool(askDef);
692
695
  const artifactDef = createWriteStageArtifactTool({
693
696
  runWorkspaceDir: roots.runWorkspaceDir,
694
- stageId: input.stage.id,
697
+ stageId: runtimeStageId(input),
695
698
  attempt: roots.attempt ?? 1,
696
699
  });
697
700
  const artifactTool = defineTool(artifactDef);
@@ -788,18 +791,18 @@ function isStageRunResult(value) {
788
791
  * Does not auto-continue the model turn — U4 owns the yield loop.
789
792
  */
790
793
  export async function reconstructStageSessionForAnswer(input, answer) {
791
- const sessionManager = await openStageSessionManager(input.roots, input.stage.id);
792
- const sessionFile = ensureStageSessionFlushed(sessionManager, input.stage.id);
794
+ const sessionManager = await openStageSessionManager(input.roots, runtimeStageId(input));
795
+ const sessionFile = ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
793
796
  let injection;
794
797
  try {
795
798
  injection = injectOpaqueAnswerIntoSession(sessionManager, answer);
796
799
  }
797
800
  catch (err) {
798
- throw new StageSessionReconstructError(`failed to inject answer into stage session: ${sessionFile}`, { stageId: input.stage.id, sessionFile, cause: err });
801
+ throw new StageSessionReconstructError(`failed to inject answer into stage session: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile, cause: err });
799
802
  }
800
803
  const wiring = await prepareStageSessionWiring(input, sessionManager);
801
804
  if (isStageRunResult(wiring)) {
802
- throw new StageSessionReconstructError(wiring.ok === false ? wiring.reason : "stage session reconstruct failed", { stageId: input.stage.id, sessionFile });
805
+ throw new StageSessionReconstructError(wiring.ok === false ? wiring.reason : "stage session reconstruct failed", { stageId: runtimeStageId(input), sessionFile });
803
806
  }
804
807
  let session;
805
808
  try {
@@ -820,7 +823,7 @@ export async function reconstructStageSessionForAnswer(input, answer) {
820
823
  modelRuntime: wiring.modelRuntime,
821
824
  });
822
825
  if (resolved.error || !resolved.model) {
823
- throw new StageSessionReconstructError(resolved.error ?? `Model not found: ${input.stage.model}`, { stageId: input.stage.id, sessionFile });
826
+ throw new StageSessionReconstructError(resolved.error ?? `Model not found: ${input.stage.model}`, { stageId: runtimeStageId(input), sessionFile });
824
827
  }
825
828
  await session.setModel(resolved.model);
826
829
  if (resolved.thinkingLevel) {
@@ -846,7 +849,7 @@ export async function reconstructStageSessionForAnswer(input, answer) {
846
849
  wiring.restoreProvider?.();
847
850
  if (err instanceof StageSessionReconstructError)
848
851
  throw err;
849
- throw new StageSessionReconstructError(`failed to reconstruct stage session: ${err instanceof Error ? err.message : String(err)}`, { stageId: input.stage.id, sessionFile, cause: err });
852
+ throw new StageSessionReconstructError(`failed to reconstruct stage session: ${err instanceof Error ? err.message : String(err)}`, { stageId: runtimeStageId(input), sessionFile, cause: err });
850
853
  }
851
854
  }
852
855
  async function bindStageSession(input, sessionManager, wiring) {
@@ -862,7 +865,7 @@ async function bindStageSession(input, sessionManager, wiring) {
862
865
  settingsManager: wiring.settingsManager,
863
866
  });
864
867
  const session = created.session;
865
- ensureStageSessionFlushed(sessionManager, input.stage.id);
868
+ ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
866
869
  await session.bindExtensions({});
867
870
  const resolved = resolveCliModel({
868
871
  cliModel: input.stage.model,
@@ -897,7 +900,7 @@ export class PiAgentAdapter {
897
900
  openStage(input) {
898
901
  const timeoutMs = input.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
899
902
  const askWaitChannel = new AskOperatorWaitChannel();
900
- const sessionFile = input.resumeToken ?? stageSessionFilePath(input.roots, input.stage.id);
903
+ const sessionFile = resolveStageSessionFile(input);
901
904
  let sessionManager;
902
905
  let resumeWaiting = false;
903
906
  if (existsSync(sessionFile)) {
@@ -906,14 +909,14 @@ export class PiAgentAdapter {
906
909
  }
907
910
  catch (err) {
908
911
  throw new StageSessionReconstructError(`stage session file corrupt or unreadable for resume: ${sessionFile}`, {
909
- stageId: input.stage.id,
912
+ stageId: runtimeStageId(input),
910
913
  sessionFile,
911
914
  cause: err,
912
915
  });
913
916
  }
914
- ensureStageSessionFlushed(sessionManager, input.stage.id);
917
+ ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
915
918
  if (!findOpenToolCall(sessionManager)) {
916
- throw new StageSessionReconstructError(`stage session has no open tool call for resume: ${sessionFile}`, { stageId: input.stage.id, sessionFile });
919
+ throw new StageSessionReconstructError(`stage session has no open tool call for resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
917
920
  }
918
921
  resumeWaiting = true;
919
922
  }
@@ -923,7 +926,7 @@ export class PiAgentAdapter {
923
926
  let prepareError;
924
927
  const preparePromise = (async () => {
925
928
  if (!sessionManager) {
926
- sessionManager = await createStageSessionManager(input.roots, input.stage.id);
929
+ sessionManager = await createStageSessionManager(input.roots, runtimeStageId(input), sessionFile);
927
930
  }
928
931
  const prepared = await prepareStageSessionWiring(input, sessionManager, askWaitChannel);
929
932
  if (isStageRunResult(prepared)) {
@@ -975,11 +978,11 @@ export class PiAgentAdapter {
975
978
  }
976
979
  };
977
980
  return createConnectedAskWaitStageHandle({
978
- stageId: input.stage.id,
981
+ stageId: runtimeStageId(input),
979
982
  askWaitChannel,
980
983
  onBeforeWaitYield: () => {
981
984
  if (sessionManager) {
982
- ensureStageSessionFlushed(sessionManager, input.stage.id);
985
+ ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
983
986
  }
984
987
  },
985
988
  run: async () => {
@@ -992,7 +995,7 @@ export class PiAgentAdapter {
992
995
  ? {
993
996
  onDeliver: (answer) => {
994
997
  if (!sessionManager) {
995
- throw new StageSessionReconstructError("stage session missing during resume deliver", { stageId: input.stage.id, sessionFile });
998
+ throw new StageSessionReconstructError("stage session missing during resume deliver", { stageId: runtimeStageId(input), sessionFile });
996
999
  }
997
1000
  injectOpaqueAnswerIntoSession(sessionManager, answer);
998
1001
  if (session) {
@@ -1002,7 +1005,7 @@ export class PiAgentAdapter {
1002
1005
  continueRun: async () => {
1003
1006
  return runWithTimeout(async () => {
1004
1007
  if (!session || !sessionManager) {
1005
- throw new StageSessionReconstructError("stage session missing during resume continue", { stageId: input.stage.id, sessionFile });
1008
+ throw new StageSessionReconstructError("stage session missing during resume continue", { stageId: runtimeStageId(input), sessionFile });
1006
1009
  }
1007
1010
  syncAgentMessagesFromSession(session, sessionManager);
1008
1011
  await session.agent.continue();
@@ -1014,7 +1017,7 @@ export class PiAgentAdapter {
1014
1017
  await preparePromise.catch(() => undefined);
1015
1018
  if (closeOptions?.park) {
1016
1019
  if (sessionManager) {
1017
- ensureStageSessionFlushed(sessionManager, input.stage.id);
1020
+ ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
1018
1021
  if (askWaitChannel.hasPending) {
1019
1022
  await repairPrematureAskOperatorClosure(sessionFile);
1020
1023
  }
@@ -1,5 +1,5 @@
1
1
  import type { StageEnvelope } from "../types/envelope.js";
2
- import type { ForkEmitContext } from "../types/forkChoice.js";
2
+ import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
3
3
  import type { StageConfig } from "../types/stage.js";
4
4
  import type { TaskFile } from "../types/task.js";
5
5
  import type { StageActivityEvent } from "./activity.js";
@@ -9,15 +9,20 @@ export type StageResumeToken = string;
9
9
  export type StageRunInput = {
10
10
  roots: StageRoots;
11
11
  stage: StageConfig;
12
+ /** Runtime instance id (`work~2`). Defaults to `stage.id` when omitted. */
13
+ stageId?: string;
12
14
  task: TaskFile;
13
15
  priorEnvelope: StageEnvelope | null;
16
+ priorEnvelopes?: StageEnvelope[];
14
17
  timeoutMs?: number;
15
18
  resumeToken?: StageResumeToken;
16
19
  /** Optional observe hook; HITL wait/answer uses openStage beside this. */
17
20
  onActivity?: (event: StageActivityEvent) => void;
18
21
  skillFilePath?: string;
19
22
  forkEmitContext?: ForkEmitContext;
23
+ cloneEmitContext?: CloneEmitContext;
20
24
  };
25
+ export declare function runtimeStageId(input: Pick<StageRunInput, "stage" | "stageId">): string;
21
26
  export type StageRunResult = {
22
27
  ok: true;
23
28
  envelope: StageEnvelope;
@@ -1,3 +1,6 @@
1
+ export function runtimeStageId(input) {
2
+ return input.stageId ?? input.stage.id;
3
+ }
1
4
  /** Thin non-HITL wrapper: open → next; unexpected wait fails closed. */
2
5
  export async function runStageViaOpen(port, input) {
3
6
  const handle = port.openStage(input);
@@ -0,0 +1,10 @@
1
+ export declare const ARTIFACT_USAGE = "Usage:\n sf artifact read --run <runId> --path <relPath> [--out <file>]";
2
+ export type ArtifactCommandIo = {
3
+ log: (line: string) => void;
4
+ error: (line: string) => void;
5
+ };
6
+ export declare function runArtifactCommand(args: string[], options?: {
7
+ cwd?: string;
8
+ projectRoot?: string;
9
+ io?: Partial<ArtifactCommandIo>;
10
+ }): Promise<number>;
@@ -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 {};