stageflow 0.9.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 +1 -0
- 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 +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -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/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 -1
- package/dist/package-meta.js +1 -1
- 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 +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- 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-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ 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
37
|
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` is running
|
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>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude session continuity marker — the adapter's equivalent of Pi's
|
|
3
|
+
* session JSONL file, minus the "reconstruct a dangling tool call" problem.
|
|
4
|
+
*
|
|
5
|
+
* Under "never let it go dangling" (see claudeAdapter.ts), by the time a
|
|
6
|
+
* stage is waiting on an operator answer, the Claude CLI subprocess has
|
|
7
|
+
* already exited cleanly on its own — there is nothing left to reopen or
|
|
8
|
+
* repair. The only state that needs to survive a process restart is small
|
|
9
|
+
* enough to write as one JSON file: the Claude `session_id` to resume, and
|
|
10
|
+
* the exact operator prompt so a later `deliverAnswer()` can be validated
|
|
11
|
+
* against it (and re-reported unchanged if `next()` is called again before
|
|
12
|
+
* an answer arrives).
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { runtimeStageId } from "./port.js";
|
|
17
|
+
/**
|
|
18
|
+
* Derives a stable, per-attempt path from `input.resumeToken` — the same
|
|
19
|
+
* value the runtime always recomputes identically for a given
|
|
20
|
+
* (workspace, stage, attempt), so a later process asking for this stage's
|
|
21
|
+
* marker lands on the same file. `resumeToken` defaults to Pi's own session
|
|
22
|
+
* path (`.../pi-session.jsonl`); this adapter writes a sibling file instead
|
|
23
|
+
* of reusing that path directly, so the two backends never share a file.
|
|
24
|
+
*/
|
|
25
|
+
export function claudeSessionMarkerPath(input) {
|
|
26
|
+
if (input.resumeToken !== undefined && input.resumeToken.trim() !== "") {
|
|
27
|
+
return path.join(path.dirname(input.resumeToken), "claude-session.json");
|
|
28
|
+
}
|
|
29
|
+
return path.join(input.roots.runWorkspaceDir, "stages", runtimeStageId(input), "claude-session.json");
|
|
30
|
+
}
|
|
31
|
+
export async function readClaudeSessionMarker(markerPath) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(markerPath, "utf8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (parsed !== null &&
|
|
36
|
+
typeof parsed === "object" &&
|
|
37
|
+
typeof parsed.sessionId === "string") {
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err.code === "ENOENT")
|
|
44
|
+
return undefined;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function writeClaudeSessionMarker(markerPath, marker) {
|
|
49
|
+
await mkdir(path.dirname(markerPath), { recursive: true });
|
|
50
|
+
await writeFile(markerPath, JSON.stringify(marker), "utf8");
|
|
51
|
+
}
|
|
52
|
+
export async function clearClaudeSessionMarker(markerPath) {
|
|
53
|
+
await rm(markerPath, { force: true });
|
|
54
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Agent SDK tool wrappers for the stage tool set.
|
|
3
|
+
*
|
|
4
|
+
* The actual envelope/artifact business logic lives in
|
|
5
|
+
* `tools/emitStageEnvelope.ts` / `tools/writeStageArtifact.ts` and is fully
|
|
6
|
+
* SDK-agnostic (typed `params: unknown`, no Pi imports) — this file reuses
|
|
7
|
+
* those `execute` functions as-is and only translates the *declaration*
|
|
8
|
+
* layer (typebox → Zod) and the *result* layer (drops Pi-only `terminate`/
|
|
9
|
+
* `details` fields, keeping just the MCP `CallToolResult` shape).
|
|
10
|
+
*
|
|
11
|
+
* `ask_operator` is different: `tools/askOperator.ts`'s own `execute` blocks
|
|
12
|
+
* on an injected wait bridge (Pi's in-process mechanism), which is exactly
|
|
13
|
+
* what Phase 3's "never let it go dangling" design avoids. This file reuses
|
|
14
|
+
* only the pure parsing/validation helpers from that module
|
|
15
|
+
* (`parseAskOperatorParams`, `normalizePromptIds`) and writes its own
|
|
16
|
+
* non-blocking handler: record the prompt on `capture`, return an immediate
|
|
17
|
+
* placeholder result, and rely on the adapter's `interrupt()` backstop
|
|
18
|
+
* (called right after this result lands) to end the turn — see
|
|
19
|
+
* claudeAdapter.ts.
|
|
20
|
+
*
|
|
21
|
+
* Registered under one in-process MCP server named "stageflow", so the model
|
|
22
|
+
* sees these as `mcp__stageflow__emit_stage_envelope`,
|
|
23
|
+
* `mcp__stageflow__write_stage_artifact`, and `mcp__stageflow__ask_operator`
|
|
24
|
+
* — those exact strings are what `composeStageUserPrompt`'s hints and the
|
|
25
|
+
* adapter's resume prompts must reference.
|
|
26
|
+
*/
|
|
27
|
+
import { type McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { type EmitCapture } from "../tools/emitStageEnvelope.js";
|
|
30
|
+
import { type WriteStageArtifactOptions } from "../tools/writeStageArtifact.js";
|
|
31
|
+
import { type AskOperatorKind, type AskOperatorPrompt } from "../tools/askOperator.js";
|
|
32
|
+
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
33
|
+
import type { PreEmitCheckOptions } from "../envelope/preEmitChecks.js";
|
|
34
|
+
export declare const STAGEFLOW_MCP_SERVER_NAME = "stageflow";
|
|
35
|
+
export declare const EMIT_STAGE_ENVELOPE_TOOL_NAME = "mcp__stageflow__emit_stage_envelope";
|
|
36
|
+
export declare const WRITE_STAGE_ARTIFACT_TOOL_NAME = "mcp__stageflow__write_stage_artifact";
|
|
37
|
+
export declare const ASK_OPERATOR_TOOL_NAME = "mcp__stageflow__ask_operator";
|
|
38
|
+
export type AskOperatorCapture = {
|
|
39
|
+
prompt?: AskOperatorPrompt;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Exported so a parity test can assert this stays field-for-field aligned
|
|
43
|
+
* with `tools/emitStageEnvelope.ts`'s typebox schema — the two are hand
|
|
44
|
+
* written against the same envelope shape with no shared source, so
|
|
45
|
+
* nothing else catches one drifting from the other.
|
|
46
|
+
*
|
|
47
|
+
* Field presence mirrors the typebox schema's own conditionality:
|
|
48
|
+
* `clone_forks` only exists when a stage can clone-fork at all, and
|
|
49
|
+
* `fork_choice` is required (not just present) when the stage has a fork
|
|
50
|
+
* to route. Building this statically would silently offer the model a
|
|
51
|
+
* `clone_forks` field on stages that have nowhere to route one.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildEmitStageEnvelopeShape(options: {
|
|
54
|
+
forkEmitContext?: ForkEmitContext;
|
|
55
|
+
cloneEmitContext?: CloneEmitContext;
|
|
56
|
+
}): {
|
|
57
|
+
status: z.ZodEnum<{
|
|
58
|
+
failure: "failure";
|
|
59
|
+
success: "success";
|
|
60
|
+
}>;
|
|
61
|
+
summary: z.ZodString;
|
|
62
|
+
artifacts: z.ZodArray<z.ZodString>;
|
|
63
|
+
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
|
+
fork_choice: z.ZodArray<z.ZodString> | z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
65
|
+
clone_forks?: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>> | undefined;
|
|
66
|
+
checklist_attestations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
67
|
+
check_id: z.ZodString;
|
|
68
|
+
items: z.ZodArray<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>>>;
|
|
70
|
+
stage_id: z.ZodOptional<z.ZodString>;
|
|
71
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
72
|
+
};
|
|
73
|
+
/** Exported for the same parity-test reason as `emitStageEnvelopeShape` above. */
|
|
74
|
+
export declare const writeStageArtifactShape: {
|
|
75
|
+
path: z.ZodString;
|
|
76
|
+
content: z.ZodString;
|
|
77
|
+
};
|
|
78
|
+
export declare function buildStageflowMcpServer(options: {
|
|
79
|
+
capture: EmitCapture;
|
|
80
|
+
payloadSchema?: unknown;
|
|
81
|
+
forkEmitContext?: ForkEmitContext;
|
|
82
|
+
cloneEmitContext?: CloneEmitContext;
|
|
83
|
+
preEmitCheckOptions?: PreEmitCheckOptions;
|
|
84
|
+
writeStageArtifact: WriteStageArtifactOptions;
|
|
85
|
+
askOperator?: {
|
|
86
|
+
capture: AskOperatorCapture;
|
|
87
|
+
allowedKinds?: readonly AskOperatorKind[];
|
|
88
|
+
};
|
|
89
|
+
}): McpServerConfig;
|