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.
- package/README.md +24 -14
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +301 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +22 -0
- package/dist/cli/runsCommand.js +773 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +43 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +10 -1
- package/dist/config/loadStage.js +138 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +49 -6
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +16 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +61 -1
- package/dist/runtime/runManager.js +501 -7
- package/dist/runtime/runRetryCoordinator.d.ts +8 -1
- package/dist/runtime/runRetryCoordinator.js +47 -10
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +79 -1
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-run/SKILL.md +56 -25
- package/skills/stageflow-run/references/mcp-call.md +2 -2
- package/skills/stageflow-run/references/native-question-ui.md +78 -0
- 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
|
-
|
|
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
|
[](https://www.npmjs.com/package/stageflow)
|
|
6
6
|
[](https://www.npmjs.com/package/stageflow)
|
|
@@ -9,7 +9,7 @@ CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badl
|
|
|
9
9
|
[](https://github.com/tejasghutukade/stageflow/issues)
|
|
10
10
|
[](https://github.com/tejasghutukade/stageflow/actions/workflows/ci.yml)
|
|
11
11
|
|
|
12
|
-
Author pipeline-owned YAML in your project. 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
|
|
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
|
+

|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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`).
|
|
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`
|
|
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
|
|
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** |
|
|
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
|
|
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
|
package/dist/agent/activity.d.ts
CHANGED
|
@@ -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>;
|