stageflow 0.8.0 → 0.12.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 (159) hide show
  1. package/README.md +24 -14
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +301 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +6 -3
  15. package/dist/agent/piAdapter.js +129 -21
  16. package/dist/agent/port.d.ts +50 -1
  17. package/dist/agent/resolveAgentPort.d.ts +26 -0
  18. package/dist/agent/resolveAgentPort.js +39 -0
  19. package/dist/cli/runCommand.js +2 -2
  20. package/dist/cli/runsCommand.d.ts +22 -0
  21. package/dist/cli/runsCommand.js +773 -0
  22. package/dist/cli/validateOutput.d.ts +2 -2
  23. package/dist/cli/validateOutput.js +2 -2
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +43 -5
  26. package/dist/config/browseCatalog.js +1 -1
  27. package/dist/config/createPipeline.d.ts +2 -1
  28. package/dist/config/createPipeline.js +34 -9
  29. package/dist/config/createStage.js +14 -9
  30. package/dist/config/loadPipeline.js +10 -1
  31. package/dist/config/loadStage.js +138 -2
  32. package/dist/config/loadStageflowManifest.js +14 -1
  33. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  34. package/dist/config/mergePipelineIncludes.js +14 -1
  35. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  36. package/dist/config/normalizePipelineStageEntry.js +49 -6
  37. package/dist/config/parseCompletionContract.d.ts +8 -0
  38. package/dist/config/parseCompletionContract.js +218 -0
  39. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  40. package/dist/config/parsePreEmitChecks.js +65 -0
  41. package/dist/config/pipelineNeeds.d.ts +19 -0
  42. package/dist/config/pipelineNeeds.js +151 -0
  43. package/dist/config/pipelineStageKeys.js +16 -1
  44. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  45. package/dist/config/resolveForkEmitContext.js +9 -2
  46. package/dist/config/resolvePipelineDag.d.ts +2 -1
  47. package/dist/config/resolvePipelineDag.js +182 -39
  48. package/dist/config/validateCatalog.d.ts +1 -1
  49. package/dist/config/validateCompletionContract.d.ts +4 -0
  50. package/dist/config/validateCompletionContract.js +52 -0
  51. package/dist/envelope/check.d.ts +1 -1
  52. package/dist/envelope/check.js +86 -13
  53. package/dist/envelope/cloneForks.d.ts +1 -1
  54. package/dist/envelope/cloneForks.js +24 -9
  55. package/dist/envelope/feedbackLoop.d.ts +4 -0
  56. package/dist/envelope/feedbackLoop.js +31 -0
  57. package/dist/envelope/payloadSchema.d.ts +7 -1
  58. package/dist/envelope/payloadSchema.js +170 -13
  59. package/dist/envelope/preEmitChecks.d.ts +29 -0
  60. package/dist/envelope/preEmitChecks.js +57 -0
  61. package/dist/hitl/qaTrail.d.ts +25 -0
  62. package/dist/hitl/qaTrail.js +51 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/mcp/catalogTools.js +4 -2
  66. package/dist/mcp/controlTools.js +72 -0
  67. package/dist/mcp/waitingGates.d.ts +2 -1
  68. package/dist/mcp/waitingGates.js +17 -1
  69. package/dist/package-meta.d.ts +1 -0
  70. package/dist/package-meta.js +1 -0
  71. package/dist/projection/projectRun.d.ts +4 -1
  72. package/dist/projection/projectRun.js +7 -0
  73. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  74. package/dist/prompt/feedbackLoopContext.js +17 -0
  75. package/dist/prompt/priorEnvelope.d.ts +2 -2
  76. package/dist/prompt/priorEnvelope.js +4 -1
  77. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  78. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  79. package/dist/runstore/port.d.ts +185 -3
  80. package/dist/runstore/port.js +42 -3
  81. package/dist/runstore/runProjection.d.ts +7 -3
  82. package/dist/runstore/runProjection.js +30 -15
  83. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  84. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  85. package/dist/runstore/sqlite/schema.d.ts +1 -1
  86. package/dist/runstore/sqlite/schema.js +99 -0
  87. package/dist/runstore/stageInstanceId.d.ts +1 -1
  88. package/dist/runstore/stageInstanceId.js +3 -2
  89. package/dist/runstore/trackProjection.js +59 -27
  90. package/dist/runstore/verificationHistory.d.ts +19 -0
  91. package/dist/runstore/verificationHistory.js +30 -0
  92. package/dist/runtime/cloneSchedule.d.ts +40 -4
  93. package/dist/runtime/cloneSchedule.js +212 -28
  94. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  95. package/dist/runtime/completionCheckRunner.js +365 -0
  96. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  97. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  98. package/dist/runtime/envelopeRouting.d.ts +10 -3
  99. package/dist/runtime/envelopeRouting.js +146 -5
  100. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  101. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  102. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  103. package/dist/runtime/feedbackLoopDecision.js +207 -0
  104. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  105. package/dist/runtime/feedbackLoopRoute.js +13 -0
  106. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  107. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  108. package/dist/runtime/forkGeneration.d.ts +72 -0
  109. package/dist/runtime/forkGeneration.js +295 -0
  110. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  111. package/dist/runtime/gitCheckoutCapability.js +97 -0
  112. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  113. package/dist/runtime/manualRecoveryState.js +73 -0
  114. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  115. package/dist/runtime/pipelineScheduler.js +328 -45
  116. package/dist/runtime/replayLifecycle.d.ts +107 -0
  117. package/dist/runtime/replayLifecycle.js +168 -0
  118. package/dist/runtime/runManager.d.ts +61 -1
  119. package/dist/runtime/runManager.js +501 -7
  120. package/dist/runtime/runRetryCoordinator.d.ts +8 -1
  121. package/dist/runtime/runRetryCoordinator.js +47 -10
  122. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  123. package/dist/runtime/stageAttemptBootstrap.js +79 -1
  124. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  125. package/dist/runtime/stageRecovery.js +1 -1
  126. package/dist/runtime/stageRunner.d.ts +4 -1
  127. package/dist/runtime/stageRunner.js +29 -3
  128. package/dist/runtime/stageWorker.js +69 -2
  129. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  130. package/dist/runtime/verifiedStageExecution.d.ts +29 -0
  131. package/dist/runtime/verifiedStageExecution.js +119 -0
  132. package/dist/server/bootstrap.js +1 -0
  133. package/dist/server/http.js +114 -0
  134. package/dist/server/operatorResults.js +2 -0
  135. package/dist/tools/askOperator.d.ts +161 -5
  136. package/dist/tools/askOperator.js +58 -25
  137. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  138. package/dist/tools/emitStageEnvelope.js +112 -24
  139. package/dist/types/completion.d.ts +43 -0
  140. package/dist/types/completion.js +1 -0
  141. package/dist/types/envelope.d.ts +18 -0
  142. package/dist/types/forkChoice.d.ts +9 -4
  143. package/dist/types/forkChoice.js +1 -1
  144. package/dist/types/pipeline.d.ts +39 -2
  145. package/dist/types/preEmitCheck.d.ts +18 -0
  146. package/dist/types/preEmitCheck.js +1 -0
  147. package/dist/types/stage.d.ts +13 -0
  148. package/dist/types/stageflowManifest.d.ts +2 -0
  149. package/dist/ui/assets/index-CXGz4nL7.js +118 -0
  150. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  151. package/dist/ui/index.html +2 -2
  152. package/package.json +2 -1
  153. package/skills/stageflow/references/control-surface.md +4 -2
  154. package/skills/stageflow-author/references/catalog-mapping.md +26 -2
  155. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  156. package/skills/stageflow-run/SKILL.md +56 -25
  157. package/skills/stageflow-run/references/mcp-call.md +2 -2
  158. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  159. package/dist/ui/assets/index-CSoGDA3A.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
 
@@ -31,14 +31,23 @@ The same pipeline runs three ways without rewriting anything:
31
31
  - **Path-based CLI** — `--pipeline` and `--task` take filesystem paths (no bare-id lookup)
32
32
  - **Pi-native** — runs on `@earendil-works/pi-coding-agent`; reuse an existing Pi login (`pi_home`) or store credentials in `~/.stageflow/agent/auth.json` (`sf_owned`)
33
33
  - **Envelope handoffs** — typed stage payloads and artifacts via `write_stage_artifact` / `emit_stage_envelope`
34
+ - **Verified stage execution** — pipeline-owned completion checks with durable evidence, automatic repair, and manual recovery ([guide](docs/verified-stage-execution.md))
34
35
  - **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
35
36
  - **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
37
+ - **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` is running
37
38
  - **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
38
39
  - **Parallel stages** — pipeline DAG with fan-out and join (see [YAML catalog](docs/yaml-catalog.md))
39
40
  - **Clonable fan-out** — clone one successor N times at completion, then join (see [YAML catalog](docs/yaml-catalog.md#clonable-successors))
40
41
  - **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
41
42
 
43
+ ## Architecture at a glance
44
+
45
+ ![Stageflow architecture: pipeline definitions and operator interfaces drive the orchestration runtime, which coordinates agent execution and persisted state](docs/img/stageflow-architecture.svg)
46
+
47
+ 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.
48
+
49
+ See [Architecture](docs/architecture.md) for component boundaries, execution flow, persistence, recovery behavior, and the tradeoffs behind fresh sessions and explicit handoffs.
50
+
42
51
  ## Installation
43
52
 
44
53
  Requires **Node.js ≥ 20**.
@@ -117,7 +126,7 @@ sf providers detect
117
126
  sf providers source set pi_home # or sf_owned
118
127
  ```
119
128
 
120
- 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`.
129
+ 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`.
121
130
 
122
131
  Full reference: [docs/providers.md](docs/providers.md)
123
132
 
@@ -126,12 +135,12 @@ Full reference: [docs/providers.md](docs/providers.md)
126
135
  Start the console with `sf ui` (default `http://127.0.0.1:3847`).
127
136
 
128
137
  - **Runs** — active and recent pipeline runs, capacity, and status at a glance
129
- - **Run detail** — stage timeline, transcripts, envelope payloads, and artifact paths
138
+ - **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
130
139
  - **HITL reply** — answer operator gates (`ask_operator`) without leaving the browser
131
140
  - **Pipelines** — browse manifest-declared pipeline definitions
132
141
  - **Settings → Providers** — connect model providers (`pi_home` or `sf_owned` credential storage)
133
142
 
134
- *Screenshot coming soon — capture after console polish lands (see `docs/img/`).*
143
+ Brand assets live under `docs/img/` (`stageflow-og.svg`, `stageflow-icon.svg`).
135
144
 
136
145
  ## Headless / CI
137
146
 
@@ -141,7 +150,7 @@ The guest actor is the CLI (`sf` / `stageflow`). `sf ui` and MCP are not require
141
150
  sf validate --strict --json
142
151
  ```
143
152
 
144
- 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.
153
+ 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.
145
154
 
146
155
  ```bash
147
156
  sf providers login <providerId> --api-key-env <VAR>
@@ -180,11 +189,9 @@ Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository
180
189
 
181
190
  ## MCP
182
191
 
183
- `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.
184
-
185
- Available tools: `list_pipelines`, `list_tasks`, `list_runs`, `get_health`, `start_run`, `get_run`, `read_artifact`.
192
+ 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.
186
193
 
187
- Full reference: [docs/mcp.md](docs/mcp.md)
194
+ HITL-aware tools include `wait_run`, `answer_gate`, and `list_waiting`. Full tool list: [docs/mcp.md](docs/mcp.md).
188
195
 
189
196
  ## Stageflow vs Conductor
190
197
 
@@ -196,7 +203,7 @@ Both projects address multi-step agent workflows. They differ in orchestration m
196
203
  | **Orchestration** | Pipeline DAG + stage worker | Jinja routing, no LLM in router |
197
204
  | **Unit of work** | Task → Pipeline → Stage attempts | Workflow → Agents |
198
205
  | **Handoff** | Typed **envelope** + artifacts | Agent output → context |
199
- | **Human gates** | Operator console + MCP | Dashboard + TUI fleet |
206
+ | **Human gates** | Console + MCP + harness native question UI | Dashboard + TUI fleet |
200
207
  | **Runtime** | Node.js, Pi coding agent | Python, Copilot/Claude SDKs |
201
208
  | **Best for** | Personal/team **multi-stage Pi workflows** you define (releases, research, SDLC, …) | Enterprise multi-agent workflows |
202
209
 
@@ -210,6 +217,7 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
210
217
  | [hello-world](examples/hello-world/) | Single stage, domain-neutral |
211
218
  | [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
212
219
  | [conditional-fork](examples/conditional-fork/) | Exclusive fork routing with operator branch choice |
220
+ | [clonable-fanout](examples/clonable-fanout/) | Clone one successor N times, then join |
213
221
  | [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
214
222
  | [ci-validate](examples/ci-validate/) | Strict validate in CI |
215
223
 
@@ -222,15 +230,17 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
222
230
  | Doc | Description |
223
231
  |-----|-------------|
224
232
  | [docs/README.md](docs/README.md) | Documentation index |
233
+ | [docs/architecture.md](docs/architecture.md) | Runtime components, execution flow, persistence, and design decisions |
225
234
  | [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
226
235
  | [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
227
- | [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf envelope`, `sf skills`, `sf validate`, `sf ui`, `sf providers` |
236
+ | [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` |
228
237
  | [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
229
238
  | [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
230
239
  | [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
231
240
  | [docs/mcp.md](docs/mcp.md) | MCP tool reference |
232
241
  | [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
233
242
  | [docs/operator-console.md](docs/operator-console.md) | Console IA and settings |
243
+ | [docs/skills-suite.md](docs/skills-suite.md) | Harness skills — router + jobs for Cursor, Claude Code, Codex, Pi, OpenCode |
234
244
  | [docs/compare-conductor.md](docs/compare-conductor.md) | Positioning deep dive |
235
245
 
236
246
  ## Develop from source
@@ -70,6 +70,15 @@ export type StageLifecycleEvent = {
70
70
  reason: string;
71
71
  } | {
72
72
  event: "skipped";
73
+ }
74
+ /** Operator explicitly authorized a new attempt after verified failure. */
75
+ | {
76
+ event: "manual_recovery_requested";
77
+ guidance?: string;
78
+ }
79
+ /** Operator chose to leave the verified failure terminal. */
80
+ | {
81
+ event: "manual_recovery_stopped";
73
82
  };
74
83
  export type StageLogLine = StageActivityEvent | StageLifecycleEvent;
75
84
  export declare function truncateActivityText(value: unknown, limit?: number): string | undefined;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Known AgentPort backend ids, and the config-hierarchy gate.
3
+ *
4
+ * Kept dependency-free (no adapter imports) so config loaders can validate
5
+ * `agent:` fields without pulling in the actual SDKs.
6
+ */
7
+ export declare const AGENT_BACKENDS: readonly ["pi", "claude"];
8
+ export type AgentBackendId = (typeof AGENT_BACKENDS)[number];
9
+ export declare function isAgentBackendId(value: string): value is AgentBackendId;
10
+ /** Narrows an already-validated config field; returns undefined for anything else. */
11
+ export declare function asAgentBackendId(value: string | undefined): AgentBackendId | undefined;
12
+ export type ParsedAgentField = {
13
+ ok: true;
14
+ value: AgentBackendId | undefined;
15
+ } | {
16
+ ok: false;
17
+ message: string;
18
+ };
19
+ /**
20
+ * Validates a raw `agent:` config value (stage, pipeline, or manifest YAML).
21
+ * Callers own their own error shape (code/category/label differ per site) —
22
+ * this only owns the one rule that must stay identical everywhere: absent is
23
+ * fine, present must be a known backend id.
24
+ */
25
+ export declare function parseAgentField(raw: unknown): ParsedAgentField;
26
+ /**
27
+ * Stage-level `agent` overrides are parsed and stored on StageConfig, but
28
+ * intentionally not consulted by resolveAgentBackend while this is false.
29
+ * Flip to true once per-stage selection is ready to ship; no other change
30
+ * needed to light it up.
31
+ */
32
+ export declare const STAGE_LEVEL_AGENT_OVERRIDE_ENABLED = false;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Known AgentPort backend ids, and the config-hierarchy gate.
3
+ *
4
+ * Kept dependency-free (no adapter imports) so config loaders can validate
5
+ * `agent:` fields without pulling in the actual SDKs.
6
+ */
7
+ export const AGENT_BACKENDS = ["pi", "claude"];
8
+ export function isAgentBackendId(value) {
9
+ return AGENT_BACKENDS.includes(value);
10
+ }
11
+ /** Narrows an already-validated config field; returns undefined for anything else. */
12
+ export function asAgentBackendId(value) {
13
+ return value !== undefined && isAgentBackendId(value) ? value : undefined;
14
+ }
15
+ /**
16
+ * Validates a raw `agent:` config value (stage, pipeline, or manifest YAML).
17
+ * Callers own their own error shape (code/category/label differ per site) —
18
+ * this only owns the one rule that must stay identical everywhere: absent is
19
+ * fine, present must be a known backend id.
20
+ */
21
+ export function parseAgentField(raw) {
22
+ if (raw === undefined)
23
+ return { ok: true, value: undefined };
24
+ if (typeof raw !== "string" || !isAgentBackendId(raw)) {
25
+ return {
26
+ ok: false,
27
+ message: `agent must be one of ${AGENT_BACKENDS.join(", ")}`,
28
+ };
29
+ }
30
+ return { ok: true, value: raw };
31
+ }
32
+ /**
33
+ * Stage-level `agent` overrides are parsed and stored on StageConfig, but
34
+ * intentionally not consulted by resolveAgentBackend while this is false.
35
+ * Flip to true once per-stage selection is ready to ship; no other change
36
+ * needed to light it up.
37
+ */
38
+ export const STAGE_LEVEL_AGENT_OVERRIDE_ENABLED = false;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Claude Agent SDK message → Pi-agnostic StageActivityEvent mapping.
3
+ *
4
+ * `tool_result` blocks in the Anthropic Messages API carry only a
5
+ * `tool_use_id`, not the tool name, so this mapper is stateful: it remembers
6
+ * the name for each in-flight tool_use id (set on `tool_start`, consumed on
7
+ * `tool_end`) the same way Pi's own session already tracks call/response
8
+ * pairing internally.
9
+ */
10
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
11
+ import { type StageActivityEvent } from "./activity.js";
12
+ export type ClaudeActivityMapper = {
13
+ map(message: SDKMessage): StageActivityEvent[];
14
+ };
15
+ export declare function createClaudeActivityMapper(): ClaudeActivityMapper;
@@ -0,0 +1,58 @@
1
+ import { truncateActivityText } from "./activity.js";
2
+ function contentBlocks(content) {
3
+ if (!Array.isArray(content))
4
+ return [];
5
+ return content.filter((block) => block !== null && typeof block === "object");
6
+ }
7
+ export function createClaudeActivityMapper() {
8
+ const toolNameById = new Map();
9
+ function map(message) {
10
+ if (message.type === "assistant") {
11
+ const events = [];
12
+ for (const block of contentBlocks(message.message.content)) {
13
+ if (block.type === "text" && typeof block.text === "string") {
14
+ events.push({
15
+ event: "message",
16
+ role: "assistant",
17
+ text: truncateActivityText(block.text),
18
+ });
19
+ }
20
+ else if (block.type === "tool_use" && typeof block.name === "string") {
21
+ const toolCallId = typeof block.id === "string" ? block.id : undefined;
22
+ if (toolCallId) {
23
+ toolNameById.set(toolCallId, block.name);
24
+ }
25
+ events.push({
26
+ event: "tool_start",
27
+ toolName: block.name,
28
+ toolCallId,
29
+ argsPreview: truncateActivityText(block.input),
30
+ });
31
+ }
32
+ }
33
+ return events;
34
+ }
35
+ if (message.type === "user") {
36
+ const events = [];
37
+ for (const block of contentBlocks(message.message.content)) {
38
+ if (block.type !== "tool_result")
39
+ continue;
40
+ const toolCallId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
41
+ const toolName = (toolCallId ? toolNameById.get(toolCallId) : undefined) ?? "unknown";
42
+ if (toolCallId) {
43
+ toolNameById.delete(toolCallId);
44
+ }
45
+ events.push({
46
+ event: "tool_end",
47
+ toolName,
48
+ toolCallId,
49
+ isError: Boolean(block.is_error),
50
+ resultPreview: truncateActivityText(block.content),
51
+ });
52
+ }
53
+ return events;
54
+ }
55
+ return [];
56
+ }
57
+ return { map };
58
+ }
@@ -0,0 +1,5 @@
1
+ import { type AgentPort, type StageHandle, type StageRunInput, type StageRunResult } from "./port.js";
2
+ export declare class ClaudeAgentAdapter implements AgentPort {
3
+ openStage(input: StageRunInput): StageHandle;
4
+ runStage(input: StageRunInput): Promise<StageRunResult>;
5
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Claude Agent SDK implementation of AgentPort.
3
+ *
4
+ * One turn = one streaming-input `query()` call, spawned as a subprocess by
5
+ * the SDK. The session is sealed the same way Pi's sealed stage session is:
6
+ * a custom (not preset) system prompt with nothing appended, no filesystem
7
+ * settings sources, and a fixed built-in tool allowlist — plus one
8
+ * in-process MCP server (`buildStageflowMcpServer`) carrying
9
+ * `emit_stage_envelope`, `write_stage_artifact`, and (when the stage's
10
+ * `gate_kinds` allow it) `ask_operator`. Permission prompts are bypassed
11
+ * outright: there is no human attached to a stage session to answer them,
12
+ * exactly like Pi's sealed session never surfaces an interactive approval
13
+ * loop either.
14
+ *
15
+ * HITL — "never let it go dangling" (confirmed by spike, see the adapter's
16
+ * plan doc): `ask_operator`'s handler never blocks. It records the prompt
17
+ * and returns an immediate placeholder result; the tool description tells
18
+ * the model to stop, and this adapter calls `query.interrupt()` right after
19
+ * observing that result as a backstop that doesn't depend on the model
20
+ * cooperating. By the time a stage reports `waiting_for_input`, the Claude
21
+ * CLI subprocess has already exited on its own — `close({park: true})` has
22
+ * nothing to flush or kill. The only state that needs to survive a process
23
+ * restart is the small JSON marker in `claudeSession.ts` (session id + the
24
+ * exact prompt), not a session file to repair.
25
+ *
26
+ * Answering resumes the *same* session (`resume: sessionId`, no
27
+ * `forkSession`) with a new user turn stating the operator's answer — the
28
+ * real prior conversation reloads (confirmed by spike), but this is a new
29
+ * turn on that history, not Pi's mid-thought `continue()`. That gap is
30
+ * permanent and documented, not a bug to chase.
31
+ *
32
+ * Explicitly NOT supported yet, rejected up front with a clear reason
33
+ * rather than silently misbehaving: `stage.skill` (Pi's `/skill:`
34
+ * convention has no Claude equivalent wired up yet) and any `stage.model`
35
+ * outside `anthropic/*` (this backend only runs Claude models).
36
+ *
37
+ * Credentials: delegated entirely to the spawned `claude` CLI's own
38
+ * environment/auth (ANTHROPIC_API_KEY or a logged-in session) — the same
39
+ * story as running `claude` by hand.
40
+ */
41
+ import { query } from "@anthropic-ai/claude-agent-sdk";
42
+ import { isAdvancingEnvelope } from "../envelope/check.js";
43
+ import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, summarizeAnswer, } from "../tools/askOperator.js";
44
+ import { buildStageflowMcpServer, EMIT_STAGE_ENVELOPE_TOOL_NAME, STAGEFLOW_MCP_SERVER_NAME, WRITE_STAGE_ARTIFACT_TOOL_NAME, ASK_OPERATOR_TOOL_NAME, } from "./claudeTools.js";
45
+ import { createClaudeActivityMapper } from "./claudeActivity.js";
46
+ import { composeStageUserPrompt } from "./piAdapter.js";
47
+ import { claudeSessionMarkerPath, clearClaudeSessionMarker, readClaudeSessionMarker, writeClaudeSessionMarker, } from "./claudeSession.js";
48
+ import { DEFAULT_STAGE_TIMEOUT_MS, runStageViaOpen, runtimeStageId, } from "./port.js";
49
+ /** Built-in tool allowlist, mirroring Pi's sealed-session base set (read/bash/write/edit). */
50
+ const CLAUDE_BUILTIN_TOOLS = ["Read", "Write", "Edit", "Bash"];
51
+ function resolveClaudeModel(stageModel) {
52
+ const slash = stageModel.indexOf("/");
53
+ if (slash < 0) {
54
+ return {
55
+ ok: false,
56
+ reason: `stage.model "${stageModel}" must be "anthropic/<model>" to run on the claude agent backend`,
57
+ };
58
+ }
59
+ const provider = stageModel.slice(0, slash);
60
+ const model = stageModel.slice(slash + 1);
61
+ if (provider !== "anthropic" || model.length === 0) {
62
+ return {
63
+ ok: false,
64
+ reason: `stage.model "${stageModel}" is not an anthropic model; the claude agent backend only runs anthropic/* models`,
65
+ };
66
+ }
67
+ return { ok: true, model };
68
+ }
69
+ function preflight(input) {
70
+ if (input.stage.skill !== undefined) {
71
+ return {
72
+ ok: false,
73
+ reason: `stage declares skill "${input.stage.skill}"; the claude agent backend does not support skills yet`,
74
+ };
75
+ }
76
+ return resolveClaudeModel(input.stage.model);
77
+ }
78
+ function resultFromCapture(capture) {
79
+ if (capture.error && !capture.envelope) {
80
+ return { ok: false, reason: capture.error };
81
+ }
82
+ if (!capture.envelope) {
83
+ return { ok: false, reason: "missing emit_stage_envelope" };
84
+ }
85
+ if (!isAdvancingEnvelope(capture.envelope)) {
86
+ return { ok: false, reason: "status: failure", envelope: capture.envelope };
87
+ }
88
+ return { ok: true, envelope: capture.envelope };
89
+ }
90
+ async function* singleUserMessage(text) {
91
+ yield {
92
+ type: "user",
93
+ message: { role: "user", content: text },
94
+ parent_tool_use_id: null,
95
+ };
96
+ }
97
+ /** One `query()` call — either the stage's initial turn or a resume turn. */
98
+ async function runTurn(input, model, markerPath, promptText, resumeSessionId) {
99
+ const timeoutMs = input.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
102
+ const activityMapper = createClaudeActivityMapper();
103
+ const emitCapture = {};
104
+ const askCapture = {};
105
+ const gateKinds = input.stage.gate_kinds;
106
+ const hitlAllowed = gateKinds === undefined || gateKinds.length > 0;
107
+ input.onActivity?.({ event: "agent_start" });
108
+ input.onActivity?.({ event: "turn_start" });
109
+ let sessionId = resumeSessionId;
110
+ try {
111
+ const mcpServer = buildStageflowMcpServer({
112
+ capture: emitCapture,
113
+ payloadSchema: input.stage.payload_schema,
114
+ forkEmitContext: input.forkEmitContext,
115
+ cloneEmitContext: input.cloneEmitContext,
116
+ preEmitCheckOptions: {
117
+ checks: input.stage.pre_emit_checks,
118
+ readQaTrail: input.readQaTrail,
119
+ },
120
+ writeStageArtifact: {
121
+ runWorkspaceDir: input.roots.runWorkspaceDir,
122
+ stageId: runtimeStageId(input),
123
+ attempt: input.roots.attempt ?? 1,
124
+ },
125
+ ...(hitlAllowed
126
+ ? {
127
+ askOperator: {
128
+ capture: askCapture,
129
+ ...(gateKinds !== undefined ? { allowedKinds: gateKinds } : {}),
130
+ },
131
+ }
132
+ : {}),
133
+ });
134
+ const stream = query({
135
+ prompt: singleUserMessage(promptText),
136
+ options: {
137
+ abortController: controller,
138
+ cwd: input.roots.cwd,
139
+ model,
140
+ systemPrompt: { type: "custom", prompt: input.stage.system_prompt },
141
+ settingSources: [],
142
+ tools: CLAUDE_BUILTIN_TOOLS,
143
+ mcpServers: { [STAGEFLOW_MCP_SERVER_NAME]: mcpServer },
144
+ permissionMode: "bypassPermissions",
145
+ allowDangerouslySkipPermissions: true,
146
+ persistSession: true,
147
+ ...(resumeSessionId !== undefined ? { resume: resumeSessionId } : {}),
148
+ },
149
+ });
150
+ for await (const message of stream) {
151
+ for (const event of activityMapper.map(message)) {
152
+ input.onActivity?.(event);
153
+ }
154
+ if (message.type === "system" && message.subtype === "init") {
155
+ sessionId = message.session_id;
156
+ await writeClaudeSessionMarker(markerPath, { sessionId: message.session_id });
157
+ }
158
+ if (message.type === "user" &&
159
+ (emitCapture.envelope !== undefined || askCapture.prompt !== undefined)) {
160
+ try {
161
+ await stream.interrupt();
162
+ }
163
+ catch {
164
+ // best-effort backstop; a natural stop may have already ended the turn
165
+ }
166
+ break;
167
+ }
168
+ if (message.type === "result") {
169
+ break;
170
+ }
171
+ }
172
+ if (askCapture.prompt !== undefined) {
173
+ const resolvedSessionId = sessionId;
174
+ if (resolvedSessionId === undefined) {
175
+ return {
176
+ kind: "completed",
177
+ result: { ok: false, reason: "ask_operator called but no session_id was observed" },
178
+ };
179
+ }
180
+ await writeClaudeSessionMarker(markerPath, {
181
+ sessionId: resolvedSessionId,
182
+ prompt: askCapture.prompt,
183
+ });
184
+ return { kind: "waiting", sessionId: resolvedSessionId, request: askCapture.prompt };
185
+ }
186
+ await clearClaudeSessionMarker(markerPath);
187
+ return { kind: "completed", result: resultFromCapture(emitCapture) };
188
+ }
189
+ catch (err) {
190
+ if (askCapture.prompt !== undefined && sessionId !== undefined) {
191
+ await writeClaudeSessionMarker(markerPath, { sessionId, prompt: askCapture.prompt });
192
+ return { kind: "waiting", sessionId, request: askCapture.prompt };
193
+ }
194
+ if (emitCapture.envelope && isAdvancingEnvelope(emitCapture.envelope)) {
195
+ await clearClaudeSessionMarker(markerPath);
196
+ return { kind: "completed", result: { ok: true, envelope: emitCapture.envelope } };
197
+ }
198
+ return {
199
+ kind: "completed",
200
+ result: {
201
+ ok: false,
202
+ reason: err instanceof Error ? err.message : String(err),
203
+ envelope: emitCapture.envelope,
204
+ },
205
+ };
206
+ }
207
+ finally {
208
+ clearTimeout(timer);
209
+ input.onActivity?.({ event: "agent_end" });
210
+ }
211
+ }
212
+ function resumePromptText(answer) {
213
+ return [
214
+ summarizeAnswer(answer),
215
+ "",
216
+ `Continue the stage. Call ${EMIT_STAGE_ENVELOPE_TOOL_NAME} when finished, or ${ASK_OPERATOR_TOOL_NAME} again if you need to ask something else.`,
217
+ ].join("\n");
218
+ }
219
+ export class ClaudeAgentAdapter {
220
+ openStage(input) {
221
+ const stageId = runtimeStageId(input);
222
+ const markerPath = claudeSessionMarkerPath(input);
223
+ let closed = false;
224
+ let bootstrapped = false;
225
+ let waiting;
226
+ let pendingAnswer;
227
+ const applyOutcome = (outcome) => {
228
+ if (outcome.kind === "waiting") {
229
+ waiting = { sessionId: outcome.sessionId, prompt: outcome.request };
230
+ pendingAnswer = undefined;
231
+ return { status: "waiting_for_input", request: outcome.request };
232
+ }
233
+ waiting = undefined;
234
+ return { status: "completed", result: outcome.result };
235
+ };
236
+ return {
237
+ stageId,
238
+ async next() {
239
+ if (closed) {
240
+ return { status: "completed", result: { ok: false, reason: "stage handle closed" } };
241
+ }
242
+ if (!bootstrapped) {
243
+ bootstrapped = true;
244
+ const marker = await readClaudeSessionMarker(markerPath);
245
+ if (marker?.prompt !== undefined) {
246
+ waiting = { sessionId: marker.sessionId, prompt: marker.prompt };
247
+ }
248
+ }
249
+ if (waiting) {
250
+ if (pendingAnswer === undefined) {
251
+ return { status: "waiting_for_input", request: waiting.prompt };
252
+ }
253
+ const rawAnswer = pendingAnswer;
254
+ pendingAnswer = undefined;
255
+ const prompt = waiting.prompt;
256
+ const sessionId = waiting.sessionId;
257
+ let parsedAnswer;
258
+ try {
259
+ parsedAnswer = parseAskOperatorAnswer(rawAnswer);
260
+ assertAnswerMatchesPrompt(prompt, parsedAnswer);
261
+ }
262
+ catch (err) {
263
+ const message = err instanceof Error ? err.message : String(err);
264
+ waiting = undefined;
265
+ await clearClaudeSessionMarker(markerPath);
266
+ return { status: "completed", result: { ok: false, reason: `invalid operator answer: ${message}` } };
267
+ }
268
+ const pre = preflight(input);
269
+ if (!pre.ok) {
270
+ waiting = undefined;
271
+ await clearClaudeSessionMarker(markerPath);
272
+ return { status: "completed", result: { ok: false, reason: pre.reason } };
273
+ }
274
+ const outcome = await runTurn(input, pre.model, markerPath, resumePromptText(parsedAnswer), sessionId);
275
+ return applyOutcome(outcome);
276
+ }
277
+ const pre = preflight(input);
278
+ if (!pre.ok) {
279
+ return { status: "completed", result: { ok: false, reason: pre.reason } };
280
+ }
281
+ const outcome = await runTurn(input, pre.model, markerPath, composeStageUserPrompt(input, EMIT_STAGE_ENVELOPE_TOOL_NAME, undefined, WRITE_STAGE_ARTIFACT_TOOL_NAME), undefined);
282
+ return applyOutcome(outcome);
283
+ },
284
+ deliverAnswer(answer) {
285
+ pendingAnswer = answer;
286
+ },
287
+ async close(closeOptions) {
288
+ closed = true;
289
+ if (closeOptions?.park) {
290
+ // Nothing to flush or kill — by the time a turn reports "waiting",
291
+ // its Claude CLI subprocess has already exited on its own and the
292
+ // marker file is already durably written.
293
+ return;
294
+ }
295
+ },
296
+ };
297
+ }
298
+ async runStage(input) {
299
+ return runStageViaOpen(this, input);
300
+ }
301
+ }
@@ -0,0 +1,18 @@
1
+ import type { AskOperatorPrompt } from "../tools/askOperator.js";
2
+ import type { StageRunInput } from "./port.js";
3
+ export type ClaudeSessionMarker = {
4
+ sessionId: string;
5
+ prompt?: AskOperatorPrompt;
6
+ };
7
+ /**
8
+ * Derives a stable, per-attempt path from `input.resumeToken` — the same
9
+ * value the runtime always recomputes identically for a given
10
+ * (workspace, stage, attempt), so a later process asking for this stage's
11
+ * marker lands on the same file. `resumeToken` defaults to Pi's own session
12
+ * path (`.../pi-session.jsonl`); this adapter writes a sibling file instead
13
+ * of reusing that path directly, so the two backends never share a file.
14
+ */
15
+ export declare function claudeSessionMarkerPath(input: StageRunInput): string;
16
+ export declare function readClaudeSessionMarker(markerPath: string): Promise<ClaudeSessionMarker | undefined>;
17
+ export declare function writeClaudeSessionMarker(markerPath: string, marker: ClaudeSessionMarker): Promise<void>;
18
+ export declare function clearClaudeSessionMarker(markerPath: string): Promise<void>;