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
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 |
|
package/dist/agent/activity.d.ts
CHANGED
|
@@ -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"
|
|
33
|
-
if (
|
|
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
|
|
38
|
-
|
|
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 =
|
|
134
|
-
if (
|
|
141
|
+
const declared = cursorExtensionEntryInPackage(packageRoot);
|
|
142
|
+
if (declared) {
|
|
135
143
|
return declared;
|
|
136
144
|
}
|
|
137
145
|
}
|
package/dist/agent/fakeAgent.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/dist/agent/piAdapter.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
792
|
-
const sessionFile = ensureStageSessionFlushed(sessionManager, input
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
912
|
+
stageId: runtimeStageId(input),
|
|
910
913
|
sessionFile,
|
|
911
914
|
cause: err,
|
|
912
915
|
});
|
|
913
916
|
}
|
|
914
|
-
ensureStageSessionFlushed(sessionManager, input
|
|
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
|
|
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
|
|
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
|
|
981
|
+
stageId: runtimeStageId(input),
|
|
979
982
|
askWaitChannel,
|
|
980
983
|
onBeforeWaitYield: () => {
|
|
981
984
|
if (sessionManager) {
|
|
982
|
-
ensureStageSessionFlushed(sessionManager, input
|
|
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
|
|
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
|
|
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
|
|
1020
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1018
1021
|
if (askWaitChannel.hasPending) {
|
|
1019
1022
|
await repairPrematureAskOperatorClosure(sessionFile);
|
|
1020
1023
|
}
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/port.js
CHANGED
|
@@ -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 {};
|