stageflow 0.16.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/README.md +2 -4
- package/dist/agent/activity.d.ts +4 -0
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/claudeTools.d.ts +1 -4
- package/dist/agent/claudeTools.js +1 -6
- package/dist/agent/fakeAgent.js +7 -6
- package/dist/agent/piAdapter.js +34 -15
- 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/config/cloneChain.d.ts +12 -0
- package/dist/config/cloneChain.js +322 -0
- package/dist/config/createPipeline.js +101 -21
- package/dist/config/createStage.js +7 -0
- package/dist/config/loadOutcome.d.ts +1 -0
- package/dist/config/loadPipeline.d.ts +2 -0
- package/dist/config/loadPipeline.js +34 -22
- package/dist/config/loadStage.d.ts +2 -0
- package/dist/config/loadStage.js +135 -125
- package/dist/config/migrateYaml.js +14 -3
- package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
- package/dist/config/normalizePipelineStageEntry.js +133 -60
- package/dist/config/parsePreEmitChecks.js +2 -2
- package/dist/config/pipelineNeeds.d.ts +15 -2
- package/dist/config/pipelineNeeds.js +86 -4
- package/dist/config/pipelineRoute.d.ts +46 -0
- package/dist/config/pipelineRoute.js +215 -0
- package/dist/config/pipelineStageKeys.d.ts +2 -0
- package/dist/config/pipelineStageKeys.js +15 -4
- package/dist/config/printTargetYaml.js +18 -12
- package/dist/config/resolveForkEmitContext.d.ts +1 -1
- package/dist/config/resolveForkEmitContext.js +3 -26
- package/dist/config/resolvePipelineDag.d.ts +1 -3
- package/dist/config/resolvePipelineDag.js +143 -186
- package/dist/config/routeIf.d.ts +24 -0
- package/dist/config/routeIf.js +247 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +5 -3
- package/dist/config/yamlDialect.d.ts +5 -8
- package/dist/config/yamlDialect.js +37 -29
- package/dist/envelope/check.d.ts +0 -2
- package/dist/envelope/check.js +1 -57
- package/dist/envelope/feedbackLoop.js +0 -3
- package/dist/envelope/forkChoice.js +1 -1
- package/dist/envelope/payloadSchema.d.ts +14 -0
- package/dist/envelope/payloadSchema.js +87 -2
- package/dist/mcp/catalogTools.js +1 -2
- package/dist/mcp/controlTools.js +17 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +0 -2
- package/dist/projection/projectRun.js +0 -3
- package/dist/prompt/priorEnvelope.js +1 -1
- package/dist/runstore/port.js +2 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -0
- package/dist/runstore/stageInstanceId.js +9 -0
- package/dist/runtime/cloneSchedule.d.ts +16 -33
- package/dist/runtime/cloneSchedule.js +155 -236
- package/dist/runtime/envelopeRouting.js +43 -94
- package/dist/runtime/forkGeneration.d.ts +6 -2
- package/dist/runtime/forkGeneration.js +45 -15
- package/dist/runtime/joinReadiness.d.ts +16 -0
- package/dist/runtime/joinReadiness.js +156 -0
- package/dist/runtime/pipelineScheduler.d.ts +0 -3
- package/dist/runtime/pipelineScheduler.js +103 -65
- 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/routeIfEval.d.ts +3 -0
- package/dist/runtime/routeIfEval.js +94 -0
- package/dist/runtime/runManager.d.ts +1 -0
- package/dist/runtime/runManager.js +76 -0
- package/dist/runtime/runRetryCoordinator.js +0 -4
- package/dist/runtime/stageAttemptBootstrap.js +60 -11
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/runtime/taskInput.js +1 -13
- 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 +3 -3
- package/dist/tools/emitStageEnvelope.js +4 -66
- package/dist/tools/prepareToolArguments.d.ts +4 -0
- package/dist/tools/prepareToolArguments.js +79 -0
- package/dist/types/envelope.d.ts +0 -2
- package/dist/types/forkChoice.d.ts +0 -16
- package/dist/types/pipeline.d.ts +86 -27
- package/dist/types/stage.d.ts +11 -7
- package/dist/ui/assets/{index-D2Wt8hwH.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 +2 -1
- package/skills/stageflow-author/SKILL.md +4 -4
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
- package/skills/stageflow-author/references/catalog-mapping.md +159 -50
- package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
- package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
- package/skills/stageflow-session-capture/SKILL.md +2 -2
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
- package/dist/envelope/cloneForks.d.ts +0 -2
- package/dist/envelope/cloneForks.js +0 -77
- package/dist/ui/assets/index-XWJovnp7.js +0 -118
package/README.md
CHANGED
|
@@ -31,13 +31,12 @@ 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** —
|
|
34
|
+
- **Verified stage execution** — after-phase `verify` checks with durable evidence, plus `on_verify_fail` repair or manual operator recovery ([guide](docs/verified-stage-execution.md))
|
|
35
35
|
- **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
|
|
36
36
|
- **Operator console** — triage runs, connect providers, answer gates, inspect transcripts at `http://127.0.0.1:3847`
|
|
37
37
|
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` is running
|
|
38
38
|
- **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
|
|
39
|
-
- **Parallel stages** — pipeline DAG with fan-out and
|
|
40
|
-
- **Clonable fan-out** — clone one successor N times at completion, then join (see [YAML catalog](docs/yaml-catalog.md#clonable-successors))
|
|
39
|
+
- **Parallel stages** — pipeline DAG with fan-out, join, and Clone Chains (one Clone Instance per Clone Array element; see [YAML catalog](docs/yaml-catalog.md#clone-chain))
|
|
41
40
|
- **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
|
|
42
41
|
|
|
43
42
|
## Architecture at a glance
|
|
@@ -219,7 +218,6 @@ If you want deterministic YAML routing across many agents, look at [Conductor](h
|
|
|
219
218
|
| [hello-world](examples/hello-world/) | Single stage, domain-neutral |
|
|
220
219
|
| [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
|
|
221
220
|
| [conditional-fork](examples/conditional-fork/) | Exclusive fork routing with operator branch choice |
|
|
222
|
-
| [clonable-fanout](examples/clonable-fanout/) | Clone one successor N times, then join |
|
|
223
221
|
| [stage-mcp](examples/stage-mcp/) | Stage MCP via project `.mcp.json` and a local echo fixture |
|
|
224
222
|
| [playwright-mcp](examples/playwright-mcp/) | One stage: open a page and save a PNG screenshot (Playwright MCP) |
|
|
225
223
|
| [context7-mcp](examples/context7-mcp/) | Three stages: resolve a library, fetch docs, write a brief (Context7 MCP) |
|
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
|
},
|
|
@@ -45,10 +45,8 @@ export type AskOperatorCapture = {
|
|
|
45
45
|
* nothing else catches one drifting from the other.
|
|
46
46
|
*
|
|
47
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
48
|
* `fork_choice` is required (not just present) when the stage has a fork
|
|
50
|
-
* to route.
|
|
51
|
-
* `clone_forks` field on stages that have nowhere to route one.
|
|
49
|
+
* to route.
|
|
52
50
|
*/
|
|
53
51
|
export declare function buildEmitStageEnvelopeShape(options: {
|
|
54
52
|
forkEmitContext?: ForkEmitContext;
|
|
@@ -62,7 +60,6 @@ export declare function buildEmitStageEnvelopeShape(options: {
|
|
|
62
60
|
artifacts: z.ZodArray<z.ZodString>;
|
|
63
61
|
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
62
|
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
63
|
checklist_attestations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
67
64
|
check_id: z.ZodString;
|
|
68
65
|
items: z.ZodArray<z.ZodString>;
|
|
@@ -40,10 +40,8 @@ export const ASK_OPERATOR_TOOL_NAME = `mcp__${STAGEFLOW_MCP_SERVER_NAME}__ask_op
|
|
|
40
40
|
* nothing else catches one drifting from the other.
|
|
41
41
|
*
|
|
42
42
|
* Field presence mirrors the typebox schema's own conditionality:
|
|
43
|
-
* `clone_forks` only exists when a stage can clone-fork at all, and
|
|
44
43
|
* `fork_choice` is required (not just present) when the stage has a fork
|
|
45
|
-
* to route.
|
|
46
|
-
* `clone_forks` field on stages that have nowhere to route one.
|
|
44
|
+
* to route.
|
|
47
45
|
*/
|
|
48
46
|
export function buildEmitStageEnvelopeShape(options) {
|
|
49
47
|
return {
|
|
@@ -54,9 +52,6 @@ export function buildEmitStageEnvelopeShape(options) {
|
|
|
54
52
|
fork_choice: options.forkEmitContext !== undefined
|
|
55
53
|
? z.array(z.string())
|
|
56
54
|
: z.array(z.string()).optional(),
|
|
57
|
-
...(options.cloneEmitContext !== undefined
|
|
58
|
-
? { clone_forks: z.array(z.record(z.string(), z.unknown())) }
|
|
59
|
-
: {}),
|
|
60
55
|
checklist_attestations: z
|
|
61
56
|
.array(z.object({
|
|
62
57
|
check_id: z.string().min(1),
|
package/dist/agent/fakeAgent.js
CHANGED
|
@@ -3,7 +3,6 @@ import path from "node:path";
|
|
|
3
3
|
import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
|
|
4
4
|
import { assertFeedbackLoopAction } from "../envelope/feedbackLoop.js";
|
|
5
5
|
import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
|
|
6
|
-
import { assertCloneForks } from "../envelope/cloneForks.js";
|
|
7
6
|
import { assertForkEnvelope } from "../envelope/forkChoice.js";
|
|
8
7
|
import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
9
8
|
import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
|
|
@@ -70,7 +69,9 @@ export class FakeAgent {
|
|
|
70
69
|
let started = false;
|
|
71
70
|
let resumeCorrupt = false;
|
|
72
71
|
const sessionMode = input.sessionMode ?? "fresh";
|
|
73
|
-
const skipHitlPark = sessionMode === "feedback_resume" ||
|
|
72
|
+
const skipHitlPark = sessionMode === "feedback_resume" ||
|
|
73
|
+
sessionMode === "timeout_resume" ||
|
|
74
|
+
sessionMode === "new_session";
|
|
74
75
|
if (sessionMode === "feedback_resume" && input.feedbackLoopContext === undefined) {
|
|
75
76
|
return createCompletedOnlyStageHandle({
|
|
76
77
|
stageId: runtimeStageId(input),
|
|
@@ -154,15 +155,15 @@ export class FakeAgent {
|
|
|
154
155
|
? behavior.envelope
|
|
155
156
|
: undefined;
|
|
156
157
|
try {
|
|
157
|
-
|
|
158
|
+
let envelope = assertRequiredEnvelope(envelopeValue);
|
|
159
|
+
if (envelope.status === "success" && envelope.payload === undefined) {
|
|
160
|
+
envelope = { ...envelope, payload: {} };
|
|
161
|
+
}
|
|
158
162
|
assertFeedbackLoopAction(envelope, input.feedbackLoopEmitContext);
|
|
159
163
|
const isFeedbackSendBack = envelope.feedback_loop?.action === "send_back";
|
|
160
164
|
if (!isFeedbackSendBack && input.forkEmitContext !== undefined) {
|
|
161
165
|
assertForkEnvelope(envelope, input.forkEmitContext);
|
|
162
166
|
}
|
|
163
|
-
if (!isFeedbackSendBack && input.cloneEmitContext !== undefined) {
|
|
164
|
-
assertCloneForks(envelope, input.cloneEmitContext);
|
|
165
|
-
}
|
|
166
167
|
assertEnvelopePayload(envelope, input.stage.payload_schema);
|
|
167
168
|
if (!isAdvancingEnvelope(envelope)) {
|
|
168
169
|
input.onActivity?.({ event: "agent_end" });
|
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).
|
|
@@ -404,23 +405,10 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
404
405
|
emitHint += `\nFailed-check evidence from attempt ${input.repairContext.prior_attempt}:\n${JSON.stringify(input.repairContext.failed_checks, null, 2)}`;
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
|
-
if (input.cloneEmitContext !== undefined) {
|
|
408
|
-
const allowedActions = input.cloneEmitContext.allowedActions ?? ["skip", "once", "fanout"];
|
|
409
|
-
const successorLines = input.cloneEmitContext.clonableSuccessors.map((successor) => {
|
|
410
|
-
const header = `- ${successor.successorId} (clone_cap: ${successor.cloneCap})`;
|
|
411
|
-
if (successor.cloneInputSchema === undefined) {
|
|
412
|
-
return header;
|
|
413
|
-
}
|
|
414
|
-
return `${header}\n Assignment schema (io.input.schema):\n${JSON.stringify(successor.cloneInputSchema, null, 2)}`;
|
|
415
|
-
});
|
|
416
|
-
emitHint += `\nClonable successors — emit clone_forks covering each of these ids exactly once:\n${successorLines.join("\n")}`;
|
|
417
|
-
emitHint += `\nEach once or fanout envelope is a full StageEnvelope — it requires status ("success" or "failure"), summary (non-empty string), and artifacts (array of strings). io.input.schema fields belong in envelope.payload, not at the top level of the clone_forks item.`;
|
|
418
|
-
emitHint += `\nAllowed clone actions: ${allowedActions.join(", ")}`;
|
|
419
|
-
}
|
|
420
408
|
if (input.feedbackLoopEmitContext !== undefined) {
|
|
421
409
|
emitHint += `\nFeedback-loop decision: this stage is a feedback-loop source. On status=success, feedback_loop is required. Emit either {"action":"continue"} to proceed downstream or {"action":"send_back","target":"…"} to request another pass.`;
|
|
422
410
|
emitHint += `\nAllowed send_back target: ${input.feedbackLoopEmitContext.target}.`;
|
|
423
|
-
emitHint += `\nA send_back decision cannot be combined with fork_choice
|
|
411
|
+
emitHint += `\nA send_back decision cannot be combined with fork_choice.`;
|
|
424
412
|
}
|
|
425
413
|
const attempt = input.roots.attempt ?? 1;
|
|
426
414
|
const attemptArtifactsPath = `stages/${runtimeStageId(input)}/attempts/${attempt}/artifacts/`;
|
|
@@ -1041,6 +1029,7 @@ export class PiAgentAdapter {
|
|
|
1041
1029
|
let sessionManager;
|
|
1042
1030
|
let resumeWaiting = false;
|
|
1043
1031
|
let feedbackResume = false;
|
|
1032
|
+
let timeoutResume = false;
|
|
1044
1033
|
let forceNewSession = sessionMode === "new_session";
|
|
1045
1034
|
if (sessionMode === "feedback_resume") {
|
|
1046
1035
|
if (input.feedbackLoopContext === undefined) {
|
|
@@ -1062,6 +1051,23 @@ export class PiAgentAdapter {
|
|
|
1062
1051
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1063
1052
|
feedbackResume = true;
|
|
1064
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
|
+
}
|
|
1065
1071
|
else if (sessionMode !== "new_session") {
|
|
1066
1072
|
if (sessionMode === "waiting_resume" || existsSync(sessionFile)) {
|
|
1067
1073
|
if (existsSync(sessionFile)) {
|
|
@@ -1128,13 +1134,17 @@ export class PiAgentAdapter {
|
|
|
1128
1134
|
const workPromise = work();
|
|
1129
1135
|
const abortPromise = new Promise((_, reject) => {
|
|
1130
1136
|
controller.signal.addEventListener("abort", () => {
|
|
1131
|
-
|
|
1137
|
+
void session?.abort();
|
|
1138
|
+
reject(new Error(stageTimeoutReason(timeoutMs)));
|
|
1132
1139
|
});
|
|
1133
1140
|
});
|
|
1134
1141
|
await Promise.race([workPromise, abortPromise]);
|
|
1135
1142
|
return { ...resultFromCapture(wiring.capture), usage: wiring.usage };
|
|
1136
1143
|
}
|
|
1137
1144
|
catch (err) {
|
|
1145
|
+
if (sessionManager) {
|
|
1146
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1147
|
+
}
|
|
1138
1148
|
if (wiring.capture.envelope && isAdvancingEnvelope(wiring.capture.envelope)) {
|
|
1139
1149
|
return { ok: true, envelope: wiring.capture.envelope, usage: wiring.usage };
|
|
1140
1150
|
}
|
|
@@ -1164,6 +1174,15 @@ export class PiAgentAdapter {
|
|
|
1164
1174
|
await session.prompt(composeFeedbackResumePrompt(input));
|
|
1165
1175
|
return;
|
|
1166
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
|
+
}
|
|
1167
1186
|
const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
|
|
1168
1187
|
await session.prompt(userPrompt);
|
|
1169
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,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PipelineStageRef, ResolvedPipelineDag } from "../types/pipeline.js";
|
|
2
|
+
import type { StageConfig } from "../types/stage.js";
|
|
3
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
4
|
+
export type DetectedCloneChain = {
|
|
5
|
+
emitterId: string;
|
|
6
|
+
cloneChildId: string;
|
|
7
|
+
joinId: string;
|
|
8
|
+
arrayField: string;
|
|
9
|
+
ref: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function detectCloneChains(stages: StageConfig[], refs: PipelineStageRef[], pipelineId: string): LoadOutcome<DetectedCloneChain[]>;
|
|
12
|
+
export declare function applyCloneChains(stages: StageConfig[], refs: PipelineStageRef[], dag: ResolvedPipelineDag, pipelineId: string): LoadOutcome<void>;
|