stageflow 0.3.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +44 -14
  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/runsCommand.d.ts +22 -0
  23. package/dist/cli/runsCommand.js +591 -0
  24. package/dist/cli/skillsCommand.d.ts +16 -0
  25. package/dist/cli/skillsCommand.js +459 -0
  26. package/dist/cli/validateOutput.d.ts +2 -2
  27. package/dist/cli/validateOutput.js +2 -2
  28. package/dist/cli.js +110 -5
  29. package/dist/config/loadPipeline.js +5 -1
  30. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  31. package/dist/config/normalizePipelineStageEntry.js +25 -1
  32. package/dist/config/pipelineStageKeys.js +1 -1
  33. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  34. package/dist/config/resolveForkEmitContext.js +23 -1
  35. package/dist/config/resolvePipelineDag.js +36 -4
  36. package/dist/envelope/check.d.ts +2 -0
  37. package/dist/envelope/check.js +51 -0
  38. package/dist/envelope/cloneForks.d.ts +2 -0
  39. package/dist/envelope/cloneForks.js +62 -0
  40. package/dist/envelope/forkChoice.js +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +3 -1
  43. package/dist/mcp/catalogTools.d.ts +3 -0
  44. package/dist/mcp/catalogTools.js +187 -0
  45. package/dist/mcp/controlTools.d.ts +3 -0
  46. package/dist/mcp/controlTools.js +147 -0
  47. package/dist/mcp/deps.d.ts +11 -0
  48. package/dist/mcp/deps.js +1 -0
  49. package/dist/mcp/projectRun.d.ts +1 -40
  50. package/dist/mcp/projectRun.js +1 -64
  51. package/dist/mcp/resources.d.ts +5 -0
  52. package/dist/mcp/resources.js +45 -0
  53. package/dist/mcp/server.d.ts +16 -3
  54. package/dist/mcp/server.js +159 -3
  55. package/dist/mcp/toolResults.d.ts +7 -0
  56. package/dist/mcp/toolResults.js +6 -0
  57. package/dist/mcp/tools.d.ts +2 -7
  58. package/dist/mcp/tools.js +52 -103
  59. package/dist/mcp/waitRun.d.ts +46 -0
  60. package/dist/mcp/waitRun.js +146 -0
  61. package/dist/mcp/waitingGates.d.ts +12 -0
  62. package/dist/mcp/waitingGates.js +83 -0
  63. package/dist/package-meta.d.ts +1 -0
  64. package/dist/package-meta.js +1 -0
  65. package/dist/projection/projectRun.d.ts +44 -0
  66. package/dist/projection/projectRun.js +71 -0
  67. package/dist/prompt/priorEnvelope.d.ts +1 -1
  68. package/dist/prompt/priorEnvelope.js +4 -1
  69. package/dist/runstore/paths.js +2 -0
  70. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  71. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  72. package/dist/runstore/port.d.ts +13 -2
  73. package/dist/runstore/port.js +4 -1
  74. package/dist/runstore/runProjection.d.ts +1 -1
  75. package/dist/runstore/runProjection.js +14 -3
  76. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  77. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  78. package/dist/runstore/stageInstanceId.d.ts +4 -0
  79. package/dist/runstore/stageInstanceId.js +23 -0
  80. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  81. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  82. package/dist/runstore/trackProjection.js +28 -12
  83. package/dist/runstore/workspaceLayout.d.ts +1 -0
  84. package/dist/runstore/workspaceLayout.js +1 -1
  85. package/dist/runtime/cloneSchedule.d.ts +18 -0
  86. package/dist/runtime/cloneSchedule.js +233 -0
  87. package/dist/runtime/envelopeRouting.d.ts +2 -1
  88. package/dist/runtime/envelopeRouting.js +80 -13
  89. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  90. package/dist/runtime/pipelineScheduler.js +166 -30
  91. package/dist/runtime/resumeReconstruct.js +8 -3
  92. package/dist/runtime/runChangeBus.d.ts +18 -0
  93. package/dist/runtime/runChangeBus.js +83 -0
  94. package/dist/runtime/runManager.d.ts +7 -0
  95. package/dist/runtime/runManager.js +31 -2
  96. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  97. package/dist/runtime/runRetryCoordinator.js +4 -1
  98. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  99. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  100. package/dist/runtime/stageRunner.d.ts +1 -0
  101. package/dist/runtime/stageRunner.js +17 -15
  102. package/dist/runtime/stageWorker.js +11 -4
  103. package/dist/server/bootstrap.d.ts +32 -0
  104. package/dist/server/bootstrap.js +56 -0
  105. package/dist/server/createHttpHost.d.ts +34 -0
  106. package/dist/server/createHttpHost.js +70 -0
  107. package/dist/server/http.d.ts +6 -13
  108. package/dist/server/http.js +295 -374
  109. package/dist/server/mcpHost.d.ts +22 -0
  110. package/dist/server/mcpHost.js +21 -0
  111. package/dist/server/operatorResults.d.ts +13 -0
  112. package/dist/server/operatorResults.js +22 -0
  113. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  114. package/dist/tools/emitStageEnvelope.js +22 -6
  115. package/dist/types/envelope.d.ts +2 -0
  116. package/dist/types/forkChoice.d.ts +22 -0
  117. package/dist/types/pipeline.d.ts +8 -0
  118. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  119. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  120. package/dist/ui/index.html +2 -2
  121. package/package.json +2 -1
  122. package/skills/install-suite.sh +107 -0
  123. package/skills/stageflow/SKILL.md +26 -0
  124. package/skills/stageflow/references/control-surface.md +28 -0
  125. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  126. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  127. package/skills/stageflow-author/SKILL.md +58 -0
  128. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  130. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  131. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  132. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  133. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  134. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  135. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  136. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  137. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  138. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  139. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  140. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  141. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  142. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  143. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  144. package/skills/stageflow-delegate/SKILL.md +18 -0
  145. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  146. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  147. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  148. package/skills/stageflow-run/SKILL.md +182 -0
  149. package/skills/stageflow-run/references/mcp-call.md +17 -0
  150. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  151. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  152. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  153. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  154. package/skills/stageflow-session-capture/SKILL.md +94 -0
  155. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  156. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  157. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  158. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  159. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  160. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  161. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  162. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  163. package/skills/stageflow-setup/SKILL.md +110 -0
  164. package/dist/ui/assets/index-CFSzDZje.js +0 -118
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # <img src="ui/public/stageflow-icon.svg" alt="" width="44" height="44" valign="middle"> Stageflow
2
2
 
3
- CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badlogic/pi-mono), with a local operator console.
3
+ Open-source runtime for **configurable multi-stage agent workflows** with typed handoffs, DAG execution, human gates, MCP, CI, and a local operator console.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/stageflow)](https://www.npmjs.com/package/stageflow)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/stageflow)](https://www.npmjs.com/package/stageflow)
@@ -9,7 +9,7 @@ CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badl
9
9
  [![GitHub issues](https://img.shields.io/github/issues/tejasghutukade/stageflow)](https://github.com/tejasghutukade/stageflow/issues)
10
10
  [![CI](https://img.shields.io/github/actions/workflow/status/tejasghutukade/stageflow/ci.yml?branch=main)](https://github.com/tejasghutukade/stageflow/actions/workflows/ci.yml)
11
11
 
12
- Author pipeline-owned YAML in your project. Stageflow runs each stage in a fresh Pi agent session, writes a structured handoff envelope, and keeps run state under `.stageflow/`. Bins: **`sf`** and **`stageflow`**.
12
+ Author pipeline-owned YAML in your project. Stageflow schedules each stage in a fresh agent session, moves context through explicit envelopes and artifacts, and persists run state under `.stageflow/`. [Pi](https://github.com/badlogic/pi-mono) is the current agent execution backend. Bins: **`sf`** and **`stageflow`**.
13
13
 
14
14
  ## Why Stageflow?
15
15
 
@@ -21,7 +21,7 @@ The same pipeline runs three ways without rewriting anything:
21
21
 
22
22
  - **Locally** — `sf ui` for triage, provider setup, and gate replies
23
23
  - **Headless / CI** — `sf validate` and `sf run --json` with predictable exit codes
24
- - **Via MCP** — Streamable HTTP tools when the console is running
24
+ - **Via MCP** — Streamable HTTP tools when `sf ui` or `sf mcp` is running
25
25
 
26
26
  **Stageflow is not an SDLC tool.** Software delivery is a popular pattern in fixtures and dogfood flows, but stages are user-authored and domain-agnostic. If you can express a multi-step workflow in YAML, Stageflow can run it on Pi.
27
27
 
@@ -33,11 +33,20 @@ The same pipeline runs three ways without rewriting anything:
33
33
  - **Envelope handoffs** — typed stage payloads and artifacts via `write_stage_artifact` / `emit_stage_envelope`
34
34
  - **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
35
35
  - **Operator console** — triage runs, connect providers, answer gates, inspect transcripts at `http://127.0.0.1:3847`
36
- - **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` is running
36
+ - **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` 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
 
42
+ ## Architecture at a glance
43
+
44
+ ![Stageflow architecture: pipeline definitions and operator interfaces drive the orchestration runtime, which coordinates agent execution and persisted state](docs/img/stageflow-architecture.svg)
45
+
46
+ The scheduler owns orchestration semantics: DAG readiness, bounded parallelism, fan-out/join, conditional routing, retries, skipped branches, and resumable human gates. Agent execution sits behind `AgentPort`; Pi supplies the current coding-agent session, while Stageflow owns pipeline state, stage workspaces, handoff validation, and the interfaces used by the CLI, console, MCP, and CI.
47
+
48
+ See [Architecture](docs/architecture.md) for component boundaries, execution flow, persistence, recovery behavior, and the tradeoffs behind fresh sessions and explicit handoffs.
49
+
41
50
  ## Installation
42
51
 
43
52
  Requires **Node.js ≥ 20**.
@@ -60,6 +69,14 @@ npm i -g ./stageflow-*.tgz
60
69
 
61
70
  `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
71
 
72
+ **Harness skills** (Cursor, Claude Code, Codex, Pi, OpenCode) — from a consumer project:
73
+
74
+ ```bash
75
+ npx skills add tejasghutukade/stageflow
76
+ ```
77
+
78
+ Then ask the agent to set up Stageflow. Details: [docs/skills-suite.md](docs/skills-suite.md).
79
+
63
80
  ## Quick start
64
81
 
65
82
  In a project directory (preferably a git repo):
@@ -108,7 +125,7 @@ sf providers detect
108
125
  sf providers source set pi_home # or sf_owned
109
126
  ```
110
127
 
111
- Stageflow is a thin Pi shell you do not need Pi CLI `/login` as a hard prerequisite if you configure providers via the console or `sf providers`.
128
+ Pi is Stageflow's current agent execution backend. Stageflow owns the workflow layer around it, and you do not need Pi CLI `/login` as a prerequisite when providers are configured through the console or `sf providers`.
112
129
 
113
130
  Full reference: [docs/providers.md](docs/providers.md)
114
131
 
@@ -117,12 +134,12 @@ Full reference: [docs/providers.md](docs/providers.md)
117
134
  Start the console with `sf ui` (default `http://127.0.0.1:3847`).
118
135
 
119
136
  - **Runs** — active and recent pipeline runs, capacity, and status at a glance
120
- - **Run detail** — stage timeline, transcripts, envelope payloads, and artifact paths
137
+ - **Run detail** — spatial stage map; select a stage for the gated workspace (logs, files, envelopes, HITL). Created runs show **not started** with a **Start run** action until history exists
121
138
  - **HITL reply** — answer operator gates (`ask_operator`) without leaving the browser
122
139
  - **Pipelines** — browse manifest-declared pipeline definitions
123
140
  - **Settings → Providers** — connect model providers (`pi_home` or `sf_owned` credential storage)
124
141
 
125
- *Screenshot coming soon capture after console polish lands (see `docs/img/`).*
142
+ Brand assets live under `docs/img/` (`stageflow-og.svg`, `stageflow-icon.svg`).
126
143
 
127
144
  ## Headless / CI
128
145
 
@@ -132,7 +149,7 @@ The guest actor is the CLI (`sf` / `stageflow`). `sf ui` and MCP are not require
132
149
  sf validate --strict --json
133
150
  ```
134
151
 
135
- Validate exits `0` or `1` only (no waiting / `2`). It checks pipeline and stage YAML only; it does not prove provider auth, Task, or checkout.
152
+ Validate exits `0` or `1` only (no waiting / `2`). With no flags, `sf validate` checks pipelines and tasks in the manifest (plus stages). `--pipeline` validates that pipeline and its stages only; `--task` validates that task file. It never proves provider auth or checkout paths.
136
153
 
137
154
  ```bash
138
155
  sf providers login <providerId> --api-key-env <VAR>
@@ -155,17 +172,25 @@ The process exits `0` when the Run succeeded, `1` when it failed (including a bu
155
172
 
156
173
  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
174
 
175
+ Post-run extraction (dogfooded in [Archify PR diagrams](examples/archify-on-pr/)):
176
+
177
+ ```bash
178
+ sf run ... --json --include stages > sf-run.json
179
+ sf envelope get --from sf-run.json --stage author-diagrams --format handoff --json
180
+ sf skills install --from-zip <url> --skill-name archify
181
+ ```
182
+
183
+ See [docs/ci.md](docs/ci.md) for the full CI recipe and [`.github/actions/sf-run`](.github/actions/sf-run) composite action.
184
+
158
185
  ## State
159
186
 
160
187
  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.
161
188
 
162
189
  ## MCP
163
190
 
164
- `sf ui` also serves a Streamable HTTP MCP endpoint at `http://127.0.0.1:3847/mcp` (URL printed on boot). Point a Cursor (or other) MCP client at that URL.
165
-
166
- Available tools: `list_pipelines`, `list_tasks`, `list_runs`, `get_health`, `start_run`, `get_run`, `read_artifact`.
191
+ Host MCP via `sf ui` or `sf mcp` at `http://127.0.0.1:3847/mcp` (URL printed on boot). Sessions are the default. Point a Cursor (or other) MCP client at that URL. Do not run both hosts against the same store.
167
192
 
168
- Full reference: [docs/mcp.md](docs/mcp.md)
193
+ HITL-aware tools include `wait_run`, `answer_gate`, and `list_waiting`. Full tool list: [docs/mcp.md](docs/mcp.md).
169
194
 
170
195
  ## Stageflow vs Conductor
171
196
 
@@ -177,7 +202,7 @@ Both projects address multi-step agent workflows. They differ in orchestration m
177
202
  | **Orchestration** | Pipeline DAG + stage worker | Jinja routing, no LLM in router |
178
203
  | **Unit of work** | Task → Pipeline → Stage attempts | Workflow → Agents |
179
204
  | **Handoff** | Typed **envelope** + artifacts | Agent output → context |
180
- | **Human gates** | Operator console + MCP | Dashboard + TUI fleet |
205
+ | **Human gates** | Console + MCP + harness native question UI | Dashboard + TUI fleet |
181
206
  | **Runtime** | Node.js, Pi coding agent | Python, Copilot/Claude SDKs |
182
207
  | **Best for** | Personal/team **multi-stage Pi workflows** you define (releases, research, SDLC, …) | Enterprise multi-agent workflows |
183
208
 
@@ -187,8 +212,11 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
187
212
 
188
213
  | Example | Description |
189
214
  |---------|-------------|
215
+ | **[archify-on-pr](examples/archify-on-pr/)** | **Featured** — PR diagram automation: conditional fork, skill binding, envelope handoff, GHA deliver |
190
216
  | [hello-world](examples/hello-world/) | Single stage, domain-neutral |
191
217
  | [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
218
+ | [conditional-fork](examples/conditional-fork/) | Exclusive fork routing with operator branch choice |
219
+ | [clonable-fanout](examples/clonable-fanout/) | Clone one successor N times, then join |
192
220
  | [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
193
221
  | [ci-validate](examples/ci-validate/) | Strict validate in CI |
194
222
 
@@ -201,15 +229,17 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
201
229
  | Doc | Description |
202
230
  |-----|-------------|
203
231
  | [docs/README.md](docs/README.md) | Documentation index |
232
+ | [docs/architecture.md](docs/architecture.md) | Runtime components, execution flow, persistence, and design decisions |
204
233
  | [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
205
234
  | [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` |
235
+ | [docs/cli-reference.md](docs/cli-reference.md) | `sf init`, `sf run`, `sf validate`, `sf ui`, `sf mcp`, `sf envelope`, `sf export-run`, `sf artifact`, `sf skills`, `sf providers` |
207
236
  | [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
208
237
  | [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
209
238
  | [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
210
239
  | [docs/mcp.md](docs/mcp.md) | MCP tool reference |
211
240
  | [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
212
241
  | [docs/operator-console.md](docs/operator-console.md) | Console IA and settings |
242
+ | [docs/skills-suite.md](docs/skills-suite.md) | Harness skills — router + jobs for Cursor, Claude Code, Codex, Pi, OpenCode |
213
243
  | [docs/compare-conductor.md](docs/compare-conductor.md) | Positioning deep dive |
214
244
 
215
245
  ## Develop from source
@@ -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>;