stageflow 0.18.0 → 0.23.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 +2 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/fakeAgent.js +3 -1
- package/dist/agent/piAdapter.js +40 -4
- package/dist/agent/port.d.ts +3 -1
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -0
- package/dist/agent/stageTimeout.d.ts +9 -0
- package/dist/agent/stageTimeout.js +24 -0
- package/dist/agent/streamLogRedact.d.ts +2 -0
- package/dist/agent/streamLogRedact.js +13 -0
- package/dist/cli/artifactCommand.js +2 -2
- package/dist/cli/envelopeCommand.js +2 -2
- package/dist/cli/exportRunCommand.js +2 -2
- package/dist/cli/graphCommand.d.ts +15 -0
- package/dist/cli/graphCommand.js +79 -0
- package/dist/cli/graphRender.d.ts +2 -0
- package/dist/cli/graphRender.js +85 -0
- package/dist/cli/hostClient.d.ts +43 -0
- package/dist/cli/hostClient.js +182 -0
- package/dist/cli/providersCommand.js +5 -3
- package/dist/cli/runCommand.d.ts +6 -5
- package/dist/cli/runCommand.js +23 -31
- package/dist/cli/runOutput.d.ts +1 -1
- package/dist/cli/runsCommand.d.ts +4 -8
- package/dist/cli/runsCommand.js +65 -89
- package/dist/cli.js +13 -5
- package/dist/config/describePipeline.d.ts +26 -0
- package/dist/config/describePipeline.js +37 -0
- package/dist/config/loadPipeline.d.ts +13 -1
- package/dist/config/loadPipeline.js +34 -4
- package/dist/config/mergePipelineIncludes.d.ts +11 -0
- package/dist/config/mergePipelineIncludes.js +75 -0
- package/dist/config/validateCatalog.d.ts +2 -2
- package/dist/config/validateCatalog.js +24 -17
- package/dist/mcp/catalogTools.js +146 -52
- package/dist/mcp/controlTools.js +94 -19
- package/dist/mcp/deps.d.ts +3 -0
- package/dist/mcp/projectMcpTools.d.ts +3 -0
- package/dist/mcp/projectMcpTools.js +25 -0
- package/dist/mcp/providerTools.d.ts +3 -0
- package/dist/mcp/providerTools.js +17 -0
- package/dist/mcp/readArtifact.d.ts +9 -0
- package/dist/mcp/readArtifact.js +11 -0
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/tailStreamLog.d.ts +8 -0
- package/dist/mcp/tailStreamLog.js +28 -0
- package/dist/mcp/toolResults.d.ts +17 -0
- package/dist/mcp/toolResults.js +15 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/waitRun.js +4 -4
- package/dist/mcp/waitingGates.d.ts +1 -0
- package/dist/mcp/waitingGates.js +3 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +3 -0
- package/dist/projection/projectRun.js +7 -0
- package/dist/runstore/port.d.ts +3 -1
- package/dist/runstore/port.js +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +3 -0
- package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
- package/dist/runtime/feedbackLoopCoordinator.js +50 -20
- package/dist/runtime/feedbackLoopDecision.js +33 -1
- package/dist/runtime/forkGeneration.d.ts +4 -0
- package/dist/runtime/forkGeneration.js +16 -0
- package/dist/runtime/pipelineRunner.d.ts +3 -3
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/pipelineScheduler.js +23 -1
- package/dist/runtime/reopenSkipped.d.ts +13 -0
- package/dist/runtime/reopenSkipped.js +56 -0
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +33 -0
- package/dist/runtime/resumeTimedOut.js +165 -0
- package/dist/runtime/runManager.d.ts +3 -1
- package/dist/runtime/runManager.js +110 -14
- package/dist/runtime/settingsFile.d.ts +2 -0
- package/dist/runtime/settingsFile.js +19 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
- package/dist/runtime/stageAttemptBootstrap.js +8 -2
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/runtime/stageStreamLog.d.ts +17 -0
- package/dist/runtime/stageStreamLog.js +89 -0
- package/dist/runtime/stageWorker.js +2 -1
- package/dist/server/bootstrap.js +9 -2
- package/dist/server/ensureGlobalService.d.ts +35 -0
- package/dist/server/ensureGlobalService.js +138 -0
- package/dist/server/http.d.ts +20 -1
- package/dist/server/http.js +452 -386
- package/dist/server/mcpHost.js +7 -8
- package/dist/server/operatorResults.d.ts +10 -0
- package/dist/server/operatorResults.js +24 -0
- package/dist/server/providerRoutes.d.ts +0 -6
- package/dist/server/providerRoutes.js +10 -18
- package/dist/tools/askOperator.d.ts +1 -0
- package/dist/tools/askOperator.js +14 -11
- package/dist/tools/emitStageEnvelope.d.ts +1 -0
- package/dist/tools/emitStageEnvelope.js +3 -1
- package/dist/tools/prepareToolArguments.d.ts +4 -0
- package/dist/tools/prepareToolArguments.js +79 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
- package/dist/ui/assets/index-Da5EHU6M.js +119 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +2 -2
- package/skills/stageflow-run/SKILL.md +27 -9
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
- package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository
|
|
|
190
190
|
|
|
191
191
|
Host MCP via `sf ui` or `sf mcp` at `http://127.0.0.1:3847/mcp` (URL printed on boot). Sessions are the default. Point a Cursor (or other) MCP client at that URL. Do not run both hosts against the same store.
|
|
192
192
|
|
|
193
|
-
HITL-aware tools include `wait_run`, `answer_gate`, and `
|
|
193
|
+
HITL-aware tools include `wait_run`, `answer_gate`, `list_waiting`, and `decide_feedback_loop`. Inspect tools (`list_providers`, `list_models`, `describe_pipeline`, …) are read-only. Full tool list: [docs/mcp.md](docs/mcp.md).
|
|
194
194
|
|
|
195
195
|
Stages use project `.mcp.json` plus stage `mcp` names. See [YAML catalog — Stage MCP](docs/yaml-catalog.md#stage-mcp).
|
|
196
196
|
|
|
@@ -222,6 +222,7 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
|
|
|
222
222
|
| [playwright-mcp](examples/playwright-mcp/) | One stage: open a page and save a PNG screenshot (Playwright MCP) |
|
|
223
223
|
| [context7-mcp](examples/context7-mcp/) | Three stages: resolve a library, fetch docs, write a brief (Context7 MCP) |
|
|
224
224
|
| [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
|
|
225
|
+
| [mcp-hitl-tour](examples/mcp-hitl-tour/) | MCP-first HITL: gates, fan-out/join, `wait_for_human` decide |
|
|
225
226
|
| [ci-validate](examples/ci-validate/) | Strict validate in CI |
|
|
226
227
|
|
|
227
228
|
Index: [examples/README.md](examples/README.md)
|
package/dist/agent/activity.d.ts
CHANGED
|
@@ -74,6 +74,10 @@ export type StageLifecycleEvent = {
|
|
|
74
74
|
} | {
|
|
75
75
|
event: "skipped";
|
|
76
76
|
}
|
|
77
|
+
/** A previously skipped stage is runnable again (parent succeeded). */
|
|
78
|
+
| {
|
|
79
|
+
event: "reopened";
|
|
80
|
+
}
|
|
77
81
|
/** Operator explicitly authorized a new attempt after verified failure. */
|
|
78
82
|
| {
|
|
79
83
|
event: "manual_recovery_requested";
|
|
@@ -82,6 +86,11 @@ export type StageLifecycleEvent = {
|
|
|
82
86
|
/** Operator chose to leave the verified failure terminal. */
|
|
83
87
|
| {
|
|
84
88
|
event: "manual_recovery_stopped";
|
|
89
|
+
} | {
|
|
90
|
+
event: "feedback_loop_decided";
|
|
91
|
+
decision: "extend" | "continue" | "abandon";
|
|
92
|
+
loopId: string;
|
|
93
|
+
reason?: string;
|
|
85
94
|
};
|
|
86
95
|
export type StageLogLine = StageActivityEvent | StageLifecycleEvent;
|
|
87
96
|
export declare function truncateActivityText(value: unknown, limit?: number): string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type StageActivityEvent } from "./activity.js";
|
|
2
2
|
export declare const TOOL_PROGRESS_THROTTLE_MS = 300;
|
|
3
|
+
export declare const STREAM_FLUSH_THROTTLE_MS = 300;
|
|
3
4
|
export type StageActivityObserver = {
|
|
4
5
|
onAssistantTextDelta(delta: string): void;
|
|
5
6
|
onThinkingDelta(delta: string): void;
|
|
@@ -10,6 +11,8 @@ export type StageActivityObserver = {
|
|
|
10
11
|
};
|
|
11
12
|
export type CreateStageActivityObserverOptions = {
|
|
12
13
|
onActivity?: (event: StageActivityEvent) => void;
|
|
14
|
+
/** Fired for every non-empty assistant text delta, regardless of writeStderr. */
|
|
15
|
+
onAssistantTextDelta?: (delta: string) => void;
|
|
13
16
|
writeStderr?: boolean;
|
|
14
17
|
toolProgressThrottleMs?: number;
|
|
15
18
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { truncateActivityText, } from "./activity.js";
|
|
2
2
|
export const TOOL_PROGRESS_THROTTLE_MS = 300;
|
|
3
|
+
export const STREAM_FLUSH_THROTTLE_MS = 300;
|
|
3
4
|
function writeIndentedPreview(preview) {
|
|
4
5
|
for (const line of preview.split("\n")) {
|
|
5
6
|
process.stderr.write(` ${line}\n`);
|
|
@@ -127,7 +128,11 @@ export function createStageActivityObserver(options = {}) {
|
|
|
127
128
|
};
|
|
128
129
|
return {
|
|
129
130
|
onAssistantTextDelta(delta) {
|
|
130
|
-
if (!
|
|
131
|
+
if (!delta) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
options.onAssistantTextDelta?.(delta);
|
|
135
|
+
if (!writeStderr) {
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
assistantStreamedForMessage = true;
|
|
@@ -47,6 +47,7 @@ import { readActivityVerbose } from "./activity.js";
|
|
|
47
47
|
import { composeStageUserPrompt } from "./piAdapter.js";
|
|
48
48
|
import { claudeSessionMarkerPath, clearClaudeSessionMarker, readClaudeSessionMarker, writeClaudeSessionMarker, } from "./claudeSession.js";
|
|
49
49
|
import { DEFAULT_STAGE_TIMEOUT_MS, runStageViaOpen, runtimeStageId, } from "./port.js";
|
|
50
|
+
import { composeTimeoutResumePrompt, stageTimeoutReason, } from "./stageTimeout.js";
|
|
50
51
|
import { StageMcpError } from "../config/resolveStageMcpServers.js";
|
|
51
52
|
import { addModelUsage, emptyStageUsage } from "../types/usage.js";
|
|
52
53
|
/** Built-in tool allowlist, mirroring Pi's sealed-session base set (read/bash/write/edit). */
|
|
@@ -295,6 +296,29 @@ async function runTurn(input, model, markerPath, promptText, resumeSessionId, us
|
|
|
295
296
|
await clearClaudeSessionMarker(markerPath);
|
|
296
297
|
return { kind: "completed", result: { ok: true, envelope: emitCapture.envelope, usage } };
|
|
297
298
|
}
|
|
299
|
+
if (controller.signal.aborted && sessionId !== undefined) {
|
|
300
|
+
await writeClaudeSessionMarker(markerPath, { sessionId, usage });
|
|
301
|
+
return {
|
|
302
|
+
kind: "completed",
|
|
303
|
+
result: {
|
|
304
|
+
ok: false,
|
|
305
|
+
reason: stageTimeoutReason(timeoutMs),
|
|
306
|
+
envelope: emitCapture.envelope,
|
|
307
|
+
usage,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (controller.signal.aborted) {
|
|
312
|
+
return {
|
|
313
|
+
kind: "completed",
|
|
314
|
+
result: {
|
|
315
|
+
ok: false,
|
|
316
|
+
reason: stageTimeoutReason(timeoutMs),
|
|
317
|
+
envelope: emitCapture.envelope,
|
|
318
|
+
usage,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
298
322
|
return {
|
|
299
323
|
kind: "completed",
|
|
300
324
|
result: {
|
|
@@ -385,6 +409,20 @@ export class ClaudeAgentAdapter {
|
|
|
385
409
|
if (!pre.ok) {
|
|
386
410
|
return { status: "completed", result: { ok: false, reason: pre.reason } };
|
|
387
411
|
}
|
|
412
|
+
if (input.sessionMode === "timeout_resume") {
|
|
413
|
+
const marker = await readClaudeSessionMarker(markerPath);
|
|
414
|
+
if (marker?.sessionId === undefined) {
|
|
415
|
+
return {
|
|
416
|
+
status: "completed",
|
|
417
|
+
result: {
|
|
418
|
+
ok: false,
|
|
419
|
+
reason: "missing Claude session to resume; use retry to start a new attempt",
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const outcome = await runTurn(input, pre.model, markerPath, composeTimeoutResumePrompt(), marker.sessionId, usage);
|
|
424
|
+
return applyOutcome(outcome);
|
|
425
|
+
}
|
|
388
426
|
const outcome = await runTurn(input, pre.model, markerPath, composeStageUserPrompt(input, EMIT_STAGE_ENVELOPE_TOOL_NAME, undefined, WRITE_STAGE_ARTIFACT_TOOL_NAME), undefined, usage);
|
|
389
427
|
return applyOutcome(outcome);
|
|
390
428
|
},
|
package/dist/agent/fakeAgent.js
CHANGED
|
@@ -69,7 +69,9 @@ export class FakeAgent {
|
|
|
69
69
|
let started = false;
|
|
70
70
|
let resumeCorrupt = false;
|
|
71
71
|
const sessionMode = input.sessionMode ?? "fresh";
|
|
72
|
-
const skipHitlPark = sessionMode === "feedback_resume" ||
|
|
72
|
+
const skipHitlPark = sessionMode === "feedback_resume" ||
|
|
73
|
+
sessionMode === "timeout_resume" ||
|
|
74
|
+
sessionMode === "new_session";
|
|
73
75
|
if (sessionMode === "feedback_resume" && input.feedbackLoopContext === undefined) {
|
|
74
76
|
return createCompletedOnlyStageHandle({
|
|
75
77
|
stageId: runtimeStageId(input),
|
package/dist/agent/piAdapter.js
CHANGED
|
@@ -48,6 +48,7 @@ import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
|
|
|
48
48
|
import { createStageActivityObserver, } from "./activityObserver.js";
|
|
49
49
|
import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
|
|
50
50
|
import { addModelUsage, emptyStageUsage } from "../types/usage.js";
|
|
51
|
+
import { TIMEOUT_ABORT_TOOL_RESULT, composeTimeoutResumePrompt, stageTimeoutReason, } from "./stageTimeout.js";
|
|
51
52
|
/**
|
|
52
53
|
* Stage tool allowlist for sealed Pi sessions.
|
|
53
54
|
* Includes `ask_operator` unless `gateKinds` is an empty list (R6).
|
|
@@ -370,8 +371,12 @@ export function routeSessionEventToProgress(event, options) {
|
|
|
370
371
|
* Map Pi session events to StageActivityEvent at the adapter edge, then
|
|
371
372
|
* forward to the activity observer (stderr + onActivity).
|
|
372
373
|
*/
|
|
373
|
-
function attachStageProgress(session, onActivity, usage) {
|
|
374
|
-
const observer = createStageActivityObserver({
|
|
374
|
+
function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
|
|
375
|
+
const observer = createStageActivityObserver({
|
|
376
|
+
onActivity,
|
|
377
|
+
onAssistantTextDelta,
|
|
378
|
+
writeStderr: true,
|
|
379
|
+
});
|
|
375
380
|
const verbose = readActivityVerbose();
|
|
376
381
|
const unsubscribe = session.subscribe((event) => {
|
|
377
382
|
routeSessionEventToProgress(event, {
|
|
@@ -1028,6 +1033,7 @@ export class PiAgentAdapter {
|
|
|
1028
1033
|
let sessionManager;
|
|
1029
1034
|
let resumeWaiting = false;
|
|
1030
1035
|
let feedbackResume = false;
|
|
1036
|
+
let timeoutResume = false;
|
|
1031
1037
|
let forceNewSession = sessionMode === "new_session";
|
|
1032
1038
|
if (sessionMode === "feedback_resume") {
|
|
1033
1039
|
if (input.feedbackLoopContext === undefined) {
|
|
@@ -1049,6 +1055,23 @@ export class PiAgentAdapter {
|
|
|
1049
1055
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1050
1056
|
feedbackResume = true;
|
|
1051
1057
|
}
|
|
1058
|
+
else if (sessionMode === "timeout_resume") {
|
|
1059
|
+
if (!existsSync(sessionFile)) {
|
|
1060
|
+
throw new StageSessionReconstructError(`stage session file missing for timeout_resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
1064
|
+
}
|
|
1065
|
+
catch (err) {
|
|
1066
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for timeout_resume: ${sessionFile}`, {
|
|
1067
|
+
stageId: runtimeStageId(input),
|
|
1068
|
+
sessionFile,
|
|
1069
|
+
cause: err,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1073
|
+
timeoutResume = true;
|
|
1074
|
+
}
|
|
1052
1075
|
else if (sessionMode !== "new_session") {
|
|
1053
1076
|
if (sessionMode === "waiting_resume" || existsSync(sessionFile)) {
|
|
1054
1077
|
if (existsSync(sessionFile)) {
|
|
@@ -1100,7 +1123,7 @@ export class PiAgentAdapter {
|
|
|
1100
1123
|
return;
|
|
1101
1124
|
}
|
|
1102
1125
|
session = bound;
|
|
1103
|
-
unsubscribeProgress = attachStageProgress(session, input.onActivity, wiring.usage);
|
|
1126
|
+
unsubscribeProgress = attachStageProgress(session, input.onActivity, wiring.usage, input.onAssistantTextDelta);
|
|
1104
1127
|
})();
|
|
1105
1128
|
const runWithTimeout = async (work) => {
|
|
1106
1129
|
await preparePromise;
|
|
@@ -1115,13 +1138,17 @@ export class PiAgentAdapter {
|
|
|
1115
1138
|
const workPromise = work();
|
|
1116
1139
|
const abortPromise = new Promise((_, reject) => {
|
|
1117
1140
|
controller.signal.addEventListener("abort", () => {
|
|
1118
|
-
|
|
1141
|
+
void session?.abort();
|
|
1142
|
+
reject(new Error(stageTimeoutReason(timeoutMs)));
|
|
1119
1143
|
});
|
|
1120
1144
|
});
|
|
1121
1145
|
await Promise.race([workPromise, abortPromise]);
|
|
1122
1146
|
return { ...resultFromCapture(wiring.capture), usage: wiring.usage };
|
|
1123
1147
|
}
|
|
1124
1148
|
catch (err) {
|
|
1149
|
+
if (sessionManager) {
|
|
1150
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1151
|
+
}
|
|
1125
1152
|
if (wiring.capture.envelope && isAdvancingEnvelope(wiring.capture.envelope)) {
|
|
1126
1153
|
return { ok: true, envelope: wiring.capture.envelope, usage: wiring.usage };
|
|
1127
1154
|
}
|
|
@@ -1151,6 +1178,15 @@ export class PiAgentAdapter {
|
|
|
1151
1178
|
await session.prompt(composeFeedbackResumePrompt(input));
|
|
1152
1179
|
return;
|
|
1153
1180
|
}
|
|
1181
|
+
if (timeoutResume) {
|
|
1182
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1183
|
+
if (findOpenToolCall(sessionManager)) {
|
|
1184
|
+
injectOpaqueAnswerIntoSession(sessionManager, TIMEOUT_ABORT_TOOL_RESULT);
|
|
1185
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1186
|
+
}
|
|
1187
|
+
await session.prompt(composeTimeoutResumePrompt());
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1154
1190
|
const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
|
|
1155
1191
|
await session.prompt(userPrompt);
|
|
1156
1192
|
});
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { QaExchange } from "../hitl/qaTrail.js";
|
|
|
11
11
|
import type { StageUsage } from "../types/usage.js";
|
|
12
12
|
/** Opaque to runtime; adapters interpret. */
|
|
13
13
|
export type StageResumeToken = string;
|
|
14
|
-
export type StageSessionMode = "fresh" | "waiting_resume" | "feedback_resume" | "new_session";
|
|
14
|
+
export type StageSessionMode = "fresh" | "waiting_resume" | "feedback_resume" | "timeout_resume" | "new_session";
|
|
15
15
|
export type StageRepairContext = {
|
|
16
16
|
prior_attempt: number;
|
|
17
17
|
/** Operator-authored instructions for an explicitly approved manual retry. */
|
|
@@ -60,6 +60,8 @@ export type StageRunInput = {
|
|
|
60
60
|
sessionMode?: StageSessionMode;
|
|
61
61
|
/** Optional observe hook; HITL wait/answer uses openStage beside this. */
|
|
62
62
|
onActivity?: (event: StageActivityEvent) => void;
|
|
63
|
+
/** Optional raw assistant-text-delta sink, fired alongside onActivity, not instead of it. */
|
|
64
|
+
onAssistantTextDelta?: (delta: string) => void;
|
|
63
65
|
skillFilePath?: string;
|
|
64
66
|
forkEmitContext?: ForkEmitContext;
|
|
65
67
|
cloneEmitContext?: CloneEmitContext;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PiHomeDetectResult, type ProviderAuthContext, type ProviderAuthStatus, type ProviderSummary, type ProvidersListResult } from "./providerAuth.js";
|
|
2
|
+
export type ProviderReadinessRow = ProviderSummary & {
|
|
3
|
+
configured: boolean;
|
|
4
|
+
authKind?: ProviderAuthStatus["authKind"];
|
|
5
|
+
source?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ProviderReadinessInspect = {
|
|
8
|
+
authShell: ProvidersListResult["authShell"];
|
|
9
|
+
via: ProvidersListResult["via"];
|
|
10
|
+
detect: PiHomeDetectResult;
|
|
11
|
+
providers: ProviderReadinessRow[];
|
|
12
|
+
};
|
|
13
|
+
export declare function inspectProviderReadiness(cwd: string, ctx?: ProviderAuthContext): Promise<ProviderReadinessInspect>;
|
|
14
|
+
export declare function mapProviderAuthError(err: unknown): {
|
|
15
|
+
status: number;
|
|
16
|
+
body: {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defaultContext, detectPiHome, getAuthStatus, listProviders, ProviderAuthError, } from "./providerAuth.js";
|
|
2
|
+
function statusList(statuses) {
|
|
3
|
+
return Array.isArray(statuses) ? statuses : [statuses];
|
|
4
|
+
}
|
|
5
|
+
export async function inspectProviderReadiness(cwd, ctx = defaultContext) {
|
|
6
|
+
const listed = await listProviders(cwd, ctx);
|
|
7
|
+
const statuses = statusList(await getAuthStatus(cwd, undefined, ctx));
|
|
8
|
+
const statusById = new Map(statuses.map((status) => [status.providerId, status]));
|
|
9
|
+
const detect = detectPiHome(cwd);
|
|
10
|
+
return {
|
|
11
|
+
authShell: listed.authShell,
|
|
12
|
+
via: listed.via,
|
|
13
|
+
detect,
|
|
14
|
+
providers: listed.providers.map((provider) => {
|
|
15
|
+
const status = statusById.get(provider.id);
|
|
16
|
+
return {
|
|
17
|
+
...provider,
|
|
18
|
+
configured: status?.configured ?? false,
|
|
19
|
+
...(status?.authKind !== undefined
|
|
20
|
+
? { authKind: status.authKind }
|
|
21
|
+
: {}),
|
|
22
|
+
...(status?.source !== undefined ? { source: status.source } : {}),
|
|
23
|
+
};
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function mapProviderAuthError(err) {
|
|
28
|
+
if (err instanceof ProviderAuthError) {
|
|
29
|
+
return { status: err.status, body: { error: err.message } };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
status: 500,
|
|
33
|
+
body: { error: "Provider auth operation failed" },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const STAGE_TIMEOUT_REASON_PREFIX = "stage timed out after ";
|
|
2
|
+
export declare function stageTimeoutReason(timeoutMs: number): string;
|
|
3
|
+
export declare function isStageTimeoutReason(reason: string | undefined): boolean;
|
|
4
|
+
export declare function lastFailedReason(events: ReadonlyArray<{
|
|
5
|
+
event: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}>): string | undefined;
|
|
8
|
+
export declare const TIMEOUT_ABORT_TOOL_RESULT = "Command aborted: the stage timed out. Continue without a debugger. Do not use --inspect or --inspect-brk.";
|
|
9
|
+
export declare function composeTimeoutResumePrompt(): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const STAGE_TIMEOUT_REASON_PREFIX = "stage timed out after ";
|
|
2
|
+
export function stageTimeoutReason(timeoutMs) {
|
|
3
|
+
return `${STAGE_TIMEOUT_REASON_PREFIX}${timeoutMs}ms`;
|
|
4
|
+
}
|
|
5
|
+
export function isStageTimeoutReason(reason) {
|
|
6
|
+
return (typeof reason === "string" && reason.startsWith(STAGE_TIMEOUT_REASON_PREFIX));
|
|
7
|
+
}
|
|
8
|
+
export function lastFailedReason(events) {
|
|
9
|
+
let reason;
|
|
10
|
+
for (const event of events) {
|
|
11
|
+
if (event.event === "failed")
|
|
12
|
+
reason = event.reason;
|
|
13
|
+
}
|
|
14
|
+
return reason;
|
|
15
|
+
}
|
|
16
|
+
export const TIMEOUT_ABORT_TOOL_RESULT = "Command aborted: the stage timed out. Continue without a debugger. Do not use --inspect or --inspect-brk.";
|
|
17
|
+
export function composeTimeoutResumePrompt() {
|
|
18
|
+
return [
|
|
19
|
+
"The previous turn was interrupted because the stage timed out.",
|
|
20
|
+
"Continue the same work from the conversation above. Do not start over.",
|
|
21
|
+
"Do not attach a debugger (no --inspect or --inspect-brk).",
|
|
22
|
+
"Finish the stage and emit the envelope when done.",
|
|
23
|
+
].join(" ");
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
3
|
+
/\bghp_[A-Za-z0-9]{20,}\b/g,
|
|
4
|
+
/\b[A-Za-z_]*KEY[A-Za-z_]*\s*[:=]\s*['"]?[A-Za-z0-9+/_-]{16,}['"]?/gi,
|
|
5
|
+
];
|
|
6
|
+
/** Scrub common secret-shaped substrings before text is persisted or returned. */
|
|
7
|
+
export function redactSecrets(text) {
|
|
8
|
+
let out = text;
|
|
9
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
10
|
+
out = out.replace(pattern, "[redacted]");
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readRunArtifact } from "../mcp/readArtifact.js";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
5
|
import { createRunStore } from "../runstore/createStore.js";
|
|
5
6
|
import { isInsideDir } from "../runstore/workspaceLayout.js";
|
|
6
7
|
export const ARTIFACT_USAGE = `Usage:
|
|
@@ -73,7 +74,6 @@ function resolveSafeOutPath(outPath, cwd) {
|
|
|
73
74
|
}
|
|
74
75
|
export async function runArtifactCommand(args, options = {}) {
|
|
75
76
|
const cwd = options.cwd ?? process.cwd();
|
|
76
|
-
const projectRoot = options.projectRoot ?? cwd;
|
|
77
77
|
const out = { ...defaultIo, ...options.io };
|
|
78
78
|
let parsed;
|
|
79
79
|
try {
|
|
@@ -99,7 +99,7 @@ export async function runArtifactCommand(args, options = {}) {
|
|
|
99
99
|
out.error(ARTIFACT_USAGE);
|
|
100
100
|
return 1;
|
|
101
101
|
}
|
|
102
|
-
const store = createRunStore({ rootDir:
|
|
102
|
+
const store = createRunStore({ rootDir: globalStageflowHome() });
|
|
103
103
|
try {
|
|
104
104
|
const contents = await readRunArtifact(store, parsed.runId, parsed.artifactPath);
|
|
105
105
|
if (parsed.outPath !== undefined) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildHandoffDeliverable, isHandoffSkipped, } from "./handoffFormat.js";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
5
|
import { createRunStore } from "../runstore/createStore.js";
|
|
5
6
|
export const ENVELOPE_USAGE = `Usage:
|
|
6
7
|
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]`;
|
|
@@ -198,7 +199,6 @@ function formatHandoffHuman(result) {
|
|
|
198
199
|
}
|
|
199
200
|
export async function runEnvelopeCommand(args, options = {}) {
|
|
200
201
|
const cwd = options.cwd ?? process.cwd();
|
|
201
|
-
const projectRoot = options.projectRoot ?? cwd;
|
|
202
202
|
const out = { ...defaultIo, ...options.io };
|
|
203
203
|
let parsed;
|
|
204
204
|
try {
|
|
@@ -224,7 +224,7 @@ export async function runEnvelopeCommand(args, options = {}) {
|
|
|
224
224
|
out.error(ENVELOPE_USAGE);
|
|
225
225
|
return 1;
|
|
226
226
|
}
|
|
227
|
-
const store = createRunStore({ rootDir:
|
|
227
|
+
const store = createRunStore({ rootDir: globalStageflowHome() });
|
|
228
228
|
let runContext;
|
|
229
229
|
try {
|
|
230
230
|
runContext = resolveRunContext(parsed, cwd, store);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { projectRun } from "../projection/projectRun.js";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
5
|
import { createRunStore } from "../runstore/createStore.js";
|
|
5
6
|
import { isInsideDir } from "../runstore/workspaceLayout.js";
|
|
6
7
|
export const EXPORT_RUN_USAGE = `Usage:
|
|
@@ -94,7 +95,6 @@ function assertRunComplete(status, runId) {
|
|
|
94
95
|
}
|
|
95
96
|
export async function runExportRunCommand(args, options = {}) {
|
|
96
97
|
const cwd = options.cwd ?? process.cwd();
|
|
97
|
-
const projectRoot = options.projectRoot ?? cwd;
|
|
98
98
|
const out = { ...defaultIo, ...options.io };
|
|
99
99
|
let parsed;
|
|
100
100
|
try {
|
|
@@ -126,7 +126,7 @@ export async function runExportRunCommand(args, options = {}) {
|
|
|
126
126
|
out.error(EXPORT_RUN_USAGE);
|
|
127
127
|
return 1;
|
|
128
128
|
}
|
|
129
|
-
const store = createRunStore({ rootDir:
|
|
129
|
+
const store = createRunStore({ rootDir: globalStageflowHome() });
|
|
130
130
|
try {
|
|
131
131
|
const detail = await store.readRun(runId);
|
|
132
132
|
assertRunComplete(detail.status, runId);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
2
|
+
export declare const GRAPH_USAGE = "Usage:\n sf graph --pipeline <path> [--json]\n\n Prints a definition-time view of how a pipeline graph is wired, before any run.\n --pipeline loads and resolves the pipeline (same resolution as sf validate / sf run).\n --json dumps the resolved DAG as JSON instead of the terminal diagram.";
|
|
3
|
+
export type GraphCommandIo = {
|
|
4
|
+
log: (line: string) => void;
|
|
5
|
+
error: (line: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function runGraphCommand(args: string[], options?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
projectRoot?: string;
|
|
10
|
+
io?: Partial<GraphCommandIo>;
|
|
11
|
+
loadPipeline?: (nameOrPath: string, opts: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
projectRoot?: string;
|
|
14
|
+
}) => Promise<LoadedPipeline>;
|
|
15
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { loadPipeline } from "../config/loadPipeline.js";
|
|
2
|
+
import { renderGraph } from "./graphRender.js";
|
|
3
|
+
export const GRAPH_USAGE = `Usage:
|
|
4
|
+
sf graph --pipeline <path> [--json]
|
|
5
|
+
|
|
6
|
+
Prints a definition-time view of how a pipeline graph is wired, before any run.
|
|
7
|
+
--pipeline loads and resolves the pipeline (same resolution as sf validate / sf run).
|
|
8
|
+
--json dumps the resolved DAG as JSON instead of the terminal diagram.`;
|
|
9
|
+
const defaultIo = {
|
|
10
|
+
log: (line) => console.log(line),
|
|
11
|
+
error: (line) => console.error(line),
|
|
12
|
+
};
|
|
13
|
+
function parseGraphArgs(args) {
|
|
14
|
+
if (args.length === 0) {
|
|
15
|
+
return { help: false, json: false };
|
|
16
|
+
}
|
|
17
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
18
|
+
return { help: true, json: false };
|
|
19
|
+
}
|
|
20
|
+
let pipeline;
|
|
21
|
+
let json = false;
|
|
22
|
+
let help = false;
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
if (arg === "--help" || arg === "-h") {
|
|
26
|
+
help = true;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === "--pipeline") {
|
|
29
|
+
const value = args[++i];
|
|
30
|
+
if (value === undefined || value.length === 0) {
|
|
31
|
+
throw new Error("Missing value for --pipeline");
|
|
32
|
+
}
|
|
33
|
+
pipeline = value;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--json") {
|
|
36
|
+
json = true;
|
|
37
|
+
}
|
|
38
|
+
else if (arg.startsWith("-")) {
|
|
39
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { help, pipeline, json };
|
|
46
|
+
}
|
|
47
|
+
export async function runGraphCommand(args, options = {}) {
|
|
48
|
+
const cwd = options.cwd ?? process.cwd();
|
|
49
|
+
const projectRoot = options.projectRoot ?? cwd;
|
|
50
|
+
const out = { ...defaultIo, ...options.io };
|
|
51
|
+
const loadPipelineFn = options.loadPipeline ?? loadPipeline;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = parseGraphArgs(args);
|
|
54
|
+
if (parsed.help) {
|
|
55
|
+
out.error(GRAPH_USAGE);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (parsed.pipeline === undefined) {
|
|
59
|
+
throw new Error("Missing value for --pipeline");
|
|
60
|
+
}
|
|
61
|
+
const loaded = await loadPipelineFn(parsed.pipeline, { cwd, projectRoot });
|
|
62
|
+
if (parsed.json) {
|
|
63
|
+
out.log(JSON.stringify(loaded.dag, null, 2));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
out.log(renderGraph(loaded.dag));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const message = err instanceof Error
|
|
71
|
+
? err.message
|
|
72
|
+
: typeof err === "string"
|
|
73
|
+
? err
|
|
74
|
+
: "Graph command failed";
|
|
75
|
+
out.error(message);
|
|
76
|
+
out.error(GRAPH_USAGE);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const MAX_WIDTH = 80;
|
|
2
|
+
// Clip to MAX_WIDTH code points so the picture fits an 80-column terminal.
|
|
3
|
+
function clampWidth(text, max = MAX_WIDTH) {
|
|
4
|
+
const chars = [...text];
|
|
5
|
+
if (chars.length <= max)
|
|
6
|
+
return text;
|
|
7
|
+
return chars.slice(0, max - 1).join("") + "…";
|
|
8
|
+
}
|
|
9
|
+
function cloneModeSuffix(node) {
|
|
10
|
+
return node.clone_mode ? ` (${node.clone_mode})` : "";
|
|
11
|
+
}
|
|
12
|
+
function buildAnnotations(node) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (node.entry)
|
|
15
|
+
parts.push("entry");
|
|
16
|
+
if (node.clone_cap !== undefined) {
|
|
17
|
+
parts.push(`clone ${node.clone_cap}${cloneModeSuffix(node)}`);
|
|
18
|
+
}
|
|
19
|
+
if (node.replay_safe === false) {
|
|
20
|
+
parts.push("replay_safe: false");
|
|
21
|
+
}
|
|
22
|
+
return parts.join(" · ");
|
|
23
|
+
}
|
|
24
|
+
// loadPipeline returns a topologically ordered DAG, so a DF walk over childrenOf yields a stable, dependency-ordered list.
|
|
25
|
+
function orderedNodeIds(dag) {
|
|
26
|
+
const visited = new Set();
|
|
27
|
+
const order = [];
|
|
28
|
+
const walk = (id) => {
|
|
29
|
+
if (visited.has(id))
|
|
30
|
+
return;
|
|
31
|
+
visited.add(id);
|
|
32
|
+
order.push(id);
|
|
33
|
+
for (const child of dag.childrenOf[id] ?? [])
|
|
34
|
+
walk(child);
|
|
35
|
+
};
|
|
36
|
+
for (const root of dag.roots)
|
|
37
|
+
walk(root);
|
|
38
|
+
for (const node of dag.nodes) {
|
|
39
|
+
if (!visited.has(node.id))
|
|
40
|
+
walk(node.id);
|
|
41
|
+
}
|
|
42
|
+
return order;
|
|
43
|
+
}
|
|
44
|
+
function cloneBandLabel(node) {
|
|
45
|
+
if (node.clone_cap === undefined)
|
|
46
|
+
return "";
|
|
47
|
+
return ` child~${node.clone_cap}${cloneModeSuffix(node)}`;
|
|
48
|
+
}
|
|
49
|
+
function loopCueLine(node) {
|
|
50
|
+
if (!node.feedback_loop)
|
|
51
|
+
return "";
|
|
52
|
+
const fl = node.feedback_loop;
|
|
53
|
+
const note = fl.on_max_replays ? ` (${fl.on_max_replays})` : "";
|
|
54
|
+
return ` ↩ loop ×${fl.max_replays} → ${fl.target}${note}`;
|
|
55
|
+
}
|
|
56
|
+
export function renderGraph(dag) {
|
|
57
|
+
const byId = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
58
|
+
const order = orderedNodeIds(dag);
|
|
59
|
+
const lines = [];
|
|
60
|
+
const push = (line) => lines.push(clampWidth(line));
|
|
61
|
+
order.forEach((id, idx) => {
|
|
62
|
+
const node = byId.get(id);
|
|
63
|
+
const marker = node.entry ? "▶ " : " ";
|
|
64
|
+
const annotations = buildAnnotations(node);
|
|
65
|
+
push(`${marker}${id}` + (annotations ? ` ${annotations}` : ""));
|
|
66
|
+
const loopCue = loopCueLine(node);
|
|
67
|
+
if (loopCue)
|
|
68
|
+
push(loopCue);
|
|
69
|
+
if (idx < order.length - 1) {
|
|
70
|
+
const children = dag.childrenOf[id] ?? [];
|
|
71
|
+
if (children.length === 1) {
|
|
72
|
+
push(" │");
|
|
73
|
+
push(` ▼${cloneBandLabel(node)}`);
|
|
74
|
+
}
|
|
75
|
+
else if (children.length > 1) {
|
|
76
|
+
children.forEach((childId, ci) => {
|
|
77
|
+
const isLast = ci === children.length - 1;
|
|
78
|
+
const branch = isLast ? "└─" : "├─";
|
|
79
|
+
push(` ${branch}▶ ${childId}${cloneBandLabel(node)}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return lines.join("\n") + "\n";
|
|
85
|
+
}
|