stageflow 0.18.0 → 0.19.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/dist/agent/activity.d.ts +4 -0
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/fakeAgent.js +3 -1
- package/dist/agent/piAdapter.js +33 -1
- package/dist/agent/port.d.ts +1 -1
- package/dist/agent/stageTimeout.d.ts +9 -0
- package/dist/agent/stageTimeout.js +24 -0
- 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/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +37 -0
- package/dist/cli.js +12 -0
- package/dist/mcp/controlTools.js +17 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.js +2 -0
- package/dist/runtime/forkGeneration.d.ts +4 -0
- package/dist/runtime/forkGeneration.js +16 -0
- 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/resumeTimedOut.d.ts +33 -0
- package/dist/runtime/resumeTimedOut.js +165 -0
- package/dist/runtime/runManager.d.ts +1 -0
- package/dist/runtime/runManager.js +76 -0
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/server/http.js +18 -0
- 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/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
- package/dist/ui/assets/index-DMVdl1qB.js +119 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
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";
|
|
@@ -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).
|
|
@@ -1028,6 +1029,7 @@ export class PiAgentAdapter {
|
|
|
1028
1029
|
let sessionManager;
|
|
1029
1030
|
let resumeWaiting = false;
|
|
1030
1031
|
let feedbackResume = false;
|
|
1032
|
+
let timeoutResume = false;
|
|
1031
1033
|
let forceNewSession = sessionMode === "new_session";
|
|
1032
1034
|
if (sessionMode === "feedback_resume") {
|
|
1033
1035
|
if (input.feedbackLoopContext === undefined) {
|
|
@@ -1049,6 +1051,23 @@ export class PiAgentAdapter {
|
|
|
1049
1051
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1050
1052
|
feedbackResume = true;
|
|
1051
1053
|
}
|
|
1054
|
+
else if (sessionMode === "timeout_resume") {
|
|
1055
|
+
if (!existsSync(sessionFile)) {
|
|
1056
|
+
throw new StageSessionReconstructError(`stage session file missing for timeout_resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for timeout_resume: ${sessionFile}`, {
|
|
1063
|
+
stageId: runtimeStageId(input),
|
|
1064
|
+
sessionFile,
|
|
1065
|
+
cause: err,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1069
|
+
timeoutResume = true;
|
|
1070
|
+
}
|
|
1052
1071
|
else if (sessionMode !== "new_session") {
|
|
1053
1072
|
if (sessionMode === "waiting_resume" || existsSync(sessionFile)) {
|
|
1054
1073
|
if (existsSync(sessionFile)) {
|
|
@@ -1115,13 +1134,17 @@ export class PiAgentAdapter {
|
|
|
1115
1134
|
const workPromise = work();
|
|
1116
1135
|
const abortPromise = new Promise((_, reject) => {
|
|
1117
1136
|
controller.signal.addEventListener("abort", () => {
|
|
1118
|
-
|
|
1137
|
+
void session?.abort();
|
|
1138
|
+
reject(new Error(stageTimeoutReason(timeoutMs)));
|
|
1119
1139
|
});
|
|
1120
1140
|
});
|
|
1121
1141
|
await Promise.race([workPromise, abortPromise]);
|
|
1122
1142
|
return { ...resultFromCapture(wiring.capture), usage: wiring.usage };
|
|
1123
1143
|
}
|
|
1124
1144
|
catch (err) {
|
|
1145
|
+
if (sessionManager) {
|
|
1146
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1147
|
+
}
|
|
1125
1148
|
if (wiring.capture.envelope && isAdvancingEnvelope(wiring.capture.envelope)) {
|
|
1126
1149
|
return { ok: true, envelope: wiring.capture.envelope, usage: wiring.usage };
|
|
1127
1150
|
}
|
|
@@ -1151,6 +1174,15 @@ export class PiAgentAdapter {
|
|
|
1151
1174
|
await session.prompt(composeFeedbackResumePrompt(input));
|
|
1152
1175
|
return;
|
|
1153
1176
|
}
|
|
1177
|
+
if (timeoutResume) {
|
|
1178
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1179
|
+
if (findOpenToolCall(sessionManager)) {
|
|
1180
|
+
injectOpaqueAnswerIntoSession(sessionManager, TIMEOUT_ABORT_TOOL_RESULT);
|
|
1181
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1182
|
+
}
|
|
1183
|
+
await session.prompt(composeTimeoutResumePrompt());
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1154
1186
|
const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
|
|
1155
1187
|
await session.prompt(userPrompt);
|
|
1156
1188
|
});
|
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. */
|
|
@@ -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,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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RunStore } from "../runstore/port.js";
|
|
2
2
|
import { RunManager } from "../runtime/runManager.js";
|
|
3
|
-
export declare const RUNS_USAGE = "Usage:\n sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]\n sf runs show --run <runId> [--from <sf-run.json>] [--json]\n sf runs verify --run <runId> --stage <stageId> [--json]\n sf runs recover --run <runId> --stage <stageId> [--guidance <text>] [--stop] [--json]\n sf runs waiting [--run <runId>] [--json]\n sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]\n sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]\n sf runs feedback-decide --run <runId> --stage <sourceStageId> [--loop <loopId>] --decision extend|continue|abandon [--reason <text>] [--json]\n sf runs retry --run <runId> --stage <stageId> [--json]\n sf runs abandon --run <runId> --stage <stageId> [--json]\n sf runs rerun --run <runId> [--json]";
|
|
3
|
+
export declare const RUNS_USAGE = "Usage:\n sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]\n sf runs show --run <runId> [--from <sf-run.json>] [--json]\n sf runs verify --run <runId> --stage <stageId> [--json]\n sf runs recover --run <runId> --stage <stageId> [--guidance <text>] [--stop] [--json]\n sf runs waiting [--run <runId>] [--json]\n sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]\n sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]\n sf runs feedback-decide --run <runId> --stage <sourceStageId> [--loop <loopId>] --decision extend|continue|abandon [--reason <text>] [--json]\n sf runs retry --run <runId> --stage <stageId> [--json]\n sf runs resume --run <runId> --stage <stageId> [--json]\n sf runs abandon --run <runId> --stage <stageId> [--json]\n sf runs rerun --run <runId> [--json]";
|
|
4
4
|
export type RunsCommandIo = {
|
|
5
5
|
log: (line: string) => void;
|
|
6
6
|
error: (line: string) => void;
|
package/dist/cli/runsCommand.js
CHANGED
|
@@ -24,6 +24,7 @@ export const RUNS_USAGE = `Usage:
|
|
|
24
24
|
sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
|
|
25
25
|
sf runs feedback-decide --run <runId> --stage <sourceStageId> [--loop <loopId>] --decision extend|continue|abandon [--reason <text>] [--json]
|
|
26
26
|
sf runs retry --run <runId> --stage <stageId> [--json]
|
|
27
|
+
sf runs resume --run <runId> --stage <stageId> [--json]
|
|
27
28
|
sf runs abandon --run <runId> --stage <stageId> [--json]
|
|
28
29
|
sf runs rerun --run <runId> [--json]`;
|
|
29
30
|
const defaultIo = {
|
|
@@ -122,6 +123,8 @@ function flagsFor(subcommand) {
|
|
|
122
123
|
return FEEDBACK_DECIDE_FLAGS;
|
|
123
124
|
case "retry":
|
|
124
125
|
return RETRY_FLAGS;
|
|
126
|
+
case "resume":
|
|
127
|
+
return RETRY_FLAGS;
|
|
125
128
|
case "abandon":
|
|
126
129
|
return ABANDON_FLAGS;
|
|
127
130
|
case "rerun":
|
|
@@ -675,6 +678,40 @@ export async function runRunsCommand(args, options = {}) {
|
|
|
675
678
|
}
|
|
676
679
|
return reportCliRun({ kind: "completion", result: result.pipeline }, { json: parsed.json, io: mutatingIo });
|
|
677
680
|
}
|
|
681
|
+
case "resume": {
|
|
682
|
+
const blocked = await guardHost();
|
|
683
|
+
if (blocked !== undefined)
|
|
684
|
+
return blocked;
|
|
685
|
+
if (!parsed.runId || !parsed.stageId) {
|
|
686
|
+
return usageError(out, "Missing --run and/or --stage");
|
|
687
|
+
}
|
|
688
|
+
const manager = buildManager();
|
|
689
|
+
const result = await manager.resumeTimedOutStage(parsed.runId, parsed.stageId);
|
|
690
|
+
if (!result.ok) {
|
|
691
|
+
if (parsed.json) {
|
|
692
|
+
printJson(out, {
|
|
693
|
+
...mapRetryStageFailure(result),
|
|
694
|
+
status: result.status,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
out.error(result.reason);
|
|
699
|
+
}
|
|
700
|
+
return 1;
|
|
701
|
+
}
|
|
702
|
+
if (parsed.json) {
|
|
703
|
+
printJson(out, {
|
|
704
|
+
ok: true,
|
|
705
|
+
runId: result.runId,
|
|
706
|
+
stageId: result.stageId,
|
|
707
|
+
attemptIndex: result.attemptIndex,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
out.log(`ok\t${result.runId}\t${result.stageId}\t${result.attemptIndex}`);
|
|
712
|
+
}
|
|
713
|
+
return 0;
|
|
714
|
+
}
|
|
678
715
|
case "recover": {
|
|
679
716
|
const blocked = await guardHost();
|
|
680
717
|
if (blocked !== undefined)
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
|
|
|
14
14
|
import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
|
|
15
15
|
import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
|
|
16
16
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
17
|
+
import { GRAPH_USAGE, runGraphCommand } from "./cli/graphCommand.js";
|
|
17
18
|
import { MIGRATE_YAML_USAGE, runMigrateYamlCommand } from "./cli/migrateYamlCommand.js";
|
|
18
19
|
import { createRunStore } from "./runstore/createStore.js";
|
|
19
20
|
import { resolveStageflowContext } from "./project/resolveStageflowContext.js";
|
|
@@ -27,6 +28,7 @@ const USAGE = `Usage:
|
|
|
27
28
|
sf init
|
|
28
29
|
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
|
|
29
30
|
sf validate [--pipeline <path>] [--task <path>] [--strict] [--json]
|
|
31
|
+
sf graph --pipeline <path> [--json]
|
|
30
32
|
sf migrate-yaml [path] [--root <path>] [--write] [--json] [--force]
|
|
31
33
|
sf artifact read --run <runId> --path <relPath> [--out <file>]
|
|
32
34
|
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
|
|
@@ -39,6 +41,7 @@ const USAGE = `Usage:
|
|
|
39
41
|
sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]
|
|
40
42
|
sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
|
|
41
43
|
sf runs retry --run <runId> --stage <stageId> [--json]
|
|
44
|
+
sf runs resume --run <runId> --stage <stageId> [--json]
|
|
42
45
|
sf runs abandon --run <runId> --stage <stageId> [--json]
|
|
43
46
|
sf runs rerun --run <runId> [--json]
|
|
44
47
|
sf ui [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
@@ -66,6 +69,8 @@ ${RUN_USAGE}
|
|
|
66
69
|
|
|
67
70
|
${VALIDATE_USAGE}
|
|
68
71
|
|
|
72
|
+
${GRAPH_USAGE}
|
|
73
|
+
|
|
69
74
|
${MIGRATE_YAML_USAGE}
|
|
70
75
|
|
|
71
76
|
${ARTIFACT_USAGE}
|
|
@@ -97,6 +102,7 @@ function parseArgs(argv) {
|
|
|
97
102
|
const command = args[0];
|
|
98
103
|
if (command === "providers" ||
|
|
99
104
|
command === "validate" ||
|
|
105
|
+
command === "graph" ||
|
|
100
106
|
command === "migrate-yaml" ||
|
|
101
107
|
command === "run" ||
|
|
102
108
|
command === "init" ||
|
|
@@ -285,6 +291,12 @@ async function main(argv) {
|
|
|
285
291
|
projectRoot: ctx.projectRoot,
|
|
286
292
|
});
|
|
287
293
|
}
|
|
294
|
+
if (parsed.command === "graph") {
|
|
295
|
+
return runGraphCommand(argv.slice(3), {
|
|
296
|
+
cwd: ctx.invocationCwd,
|
|
297
|
+
projectRoot: ctx.projectRoot,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
288
300
|
if (parsed.command === "migrate-yaml") {
|
|
289
301
|
return runMigrateYamlCommand(argv.slice(3), {
|
|
290
302
|
cwd: ctx.invocationCwd,
|
package/dist/mcp/controlTools.js
CHANGED
|
@@ -183,6 +183,23 @@ export function registerControlTools(server, deps) {
|
|
|
183
183
|
attemptIndex: result.attemptIndex,
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
+
server.registerTool("resume_stage", {
|
|
187
|
+
description: "Resume a stage that failed because it timed out, continuing the same attempt/session (same as POST .../resume). Does not start a new attempt — use retry_stage to start over.",
|
|
188
|
+
inputSchema: z.object({
|
|
189
|
+
runId: z.string(),
|
|
190
|
+
stageId: z.string(),
|
|
191
|
+
}),
|
|
192
|
+
}, async ({ runId, stageId }) => {
|
|
193
|
+
const result = await manager.resumeTimedOutStage(runId, stageId);
|
|
194
|
+
if (!result.ok) {
|
|
195
|
+
return textResult({ ...mapRetryStageFailure(result), status: result.status }, true);
|
|
196
|
+
}
|
|
197
|
+
return textResult({
|
|
198
|
+
runId: result.runId,
|
|
199
|
+
stageId: result.stageId,
|
|
200
|
+
attemptIndex: result.attemptIndex,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
186
203
|
server.registerTool("abandon_stage", {
|
|
187
204
|
description: "Abandon a running stage (marks it failed/interrupted). Does not dismiss HITL — waiting stages return 409; answer them with answer_gate. There is no run-level cancel tool.",
|
|
188
205
|
inputSchema: z.object({
|
package/dist/package-meta.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.19.0";
|
package/dist/package-meta.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export const PACKAGE_VERSION = "0.
|
|
2
|
+
export const PACKAGE_VERSION = "0.19.0";
|
package/dist/runstore/port.js
CHANGED
|
@@ -63,6 +63,10 @@ export declare function selectActiveInput(options: {
|
|
|
63
63
|
needsStageId: string;
|
|
64
64
|
scheduleOverride?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
65
65
|
}): Promise<ActiveCohort>;
|
|
66
|
+
export declare function forkCohortMapsFromStore(store: Pick<RunStore, "listForkGenerations">, runId: string): Promise<{
|
|
67
|
+
supersededCloneIds: Set<string>;
|
|
68
|
+
cohortByForkParent: Map<string, Set<string>>;
|
|
69
|
+
}>;
|
|
66
70
|
export declare function filterJoinInputs(allInstanceIds: readonly string[], cohort: ActiveCohort): string[];
|
|
67
71
|
export declare function filterJoinInstanceIds(allInstanceIds: readonly string[], activeCloneIds: Set<string> | null): string[];
|
|
68
72
|
/**
|
|
@@ -251,6 +251,22 @@ export async function selectActiveInput(options) {
|
|
|
251
251
|
}
|
|
252
252
|
return activeCohortForParent(options.store, options.runId, forkParentId);
|
|
253
253
|
}
|
|
254
|
+
export async function forkCohortMapsFromStore(store, runId) {
|
|
255
|
+
const gens = await store.listForkGenerations(runId);
|
|
256
|
+
const supersededCloneIds = new Set();
|
|
257
|
+
const cohortByForkParent = new Map();
|
|
258
|
+
for (const gen of gens) {
|
|
259
|
+
if (gen.status === "superseded") {
|
|
260
|
+
for (const id of gen.clone_stage_ids) {
|
|
261
|
+
supersededCloneIds.add(id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (gen.status === "active") {
|
|
265
|
+
cohortByForkParent.set(gen.fork_parent_stage_id, new Set(gen.clone_stage_ids));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { supersededCloneIds, cohortByForkParent };
|
|
269
|
+
}
|
|
254
270
|
export function filterJoinInputs(allInstanceIds, cohort) {
|
|
255
271
|
if (cohort.kind === "untracked")
|
|
256
272
|
return [...allInstanceIds];
|
|
@@ -4,7 +4,8 @@ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
|
4
4
|
import { cloneFailureContinuesSchedule, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, failureIsAccepted, isCloneInstance, mint, protectedClonableChildIds, sequentialLaterCloneIds, skipRejectedNeedDependents, } from "./cloneSchedule.js";
|
|
5
5
|
import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
|
|
6
6
|
import { classifyInboundAfterSuccess, isEagerSingleParentIfSkip, joinAllowsRun, pickStalledJoinSkips, } from "./joinReadiness.js";
|
|
7
|
-
import {
|
|
7
|
+
import { reopenRunnableSkippedStages, persistReopenedStages } from "./reopenSkipped.js";
|
|
8
|
+
import { forkChoicePreservesReplaySource, forkCohortMapsFromStore, mintCohortForFanout, } from "./forkGeneration.js";
|
|
8
9
|
import { activeReplayId, activeSourceStageId, applySendBack, createReplaySchedule, hydrateFromStore, isHeld, launchAttemptFor, launchFor, noteActiveCohort, onContinued, onRouteStageFailed, onRouteStageRunning, onRouteStageSucceeded, rebindRouteAttempts, resolveDecision, } from "./replayLifecycle.js";
|
|
9
10
|
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
10
11
|
import { runStage, isRunStageWaiting } from "./stageRunner.js";
|
|
@@ -65,6 +66,7 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
|
|
|
65
66
|
continue;
|
|
66
67
|
schedulingHalted = true;
|
|
67
68
|
}
|
|
69
|
+
await reopenRunnableSkippedStages(store, runId, resolvedDag, states, completedEnvelopes);
|
|
68
70
|
return {
|
|
69
71
|
states,
|
|
70
72
|
completedEnvelopes,
|
|
@@ -85,15 +87,20 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
const retryRootSet = new Set(retryStageIds);
|
|
90
|
+
const { supersededCloneIds } = await forkCohortMapsFromStore(store, runId);
|
|
88
91
|
const states = new Map();
|
|
89
92
|
for (const node of resolvedDag.nodes) {
|
|
90
93
|
states.set(node.id, "pending");
|
|
91
94
|
}
|
|
92
95
|
const overrides = new Map();
|
|
93
96
|
for (const rootId of retryStageIds) {
|
|
97
|
+
if (supersededCloneIds.has(rootId))
|
|
98
|
+
continue;
|
|
94
99
|
overrides.set(rootId, "pending");
|
|
95
100
|
}
|
|
96
101
|
for (const id of downstream) {
|
|
102
|
+
if (supersededCloneIds.has(id))
|
|
103
|
+
continue;
|
|
97
104
|
overrides.set(id, "pending");
|
|
98
105
|
}
|
|
99
106
|
for (const snap of runDetail.stages) {
|
|
@@ -104,6 +111,20 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
|
|
|
104
111
|
completedEnvelopes.delete(id);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
114
|
+
const skippedToReopen = [];
|
|
115
|
+
const skipStates = new Map();
|
|
116
|
+
for (const snap of runDetail.stages) {
|
|
117
|
+
const id = snap.stage_id;
|
|
118
|
+
if (supersededCloneIds.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
if (snap.status !== "skipped")
|
|
121
|
+
continue;
|
|
122
|
+
if (!retryRootSet.has(id) && !downstream.has(id))
|
|
123
|
+
continue;
|
|
124
|
+
skippedToReopen.push(id);
|
|
125
|
+
skipStates.set(id, "skipped");
|
|
126
|
+
}
|
|
127
|
+
await persistReopenedStages(store, runId, skippedToReopen, skipStates);
|
|
107
128
|
return {
|
|
108
129
|
states,
|
|
109
130
|
completedEnvelopes,
|
|
@@ -752,6 +773,7 @@ export async function runPipelineDag(options) {
|
|
|
752
773
|
await persistSkipPending(childId);
|
|
753
774
|
}
|
|
754
775
|
}
|
|
776
|
+
await reopenRunnableSkippedStages(store, run.runId, dag, states, completedEnvelopes);
|
|
755
777
|
};
|
|
756
778
|
const handlePostSuccessError = async (stageId, reason) => {
|
|
757
779
|
if (states.get(stageId) === "succeeded") {
|