stageflow 0.3.0 → 0.8.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 +22 -1
- package/dist/agent/activity.d.ts +2 -0
- package/dist/agent/cursorProvider.d.ts +1 -10
- package/dist/agent/cursorProvider.js +18 -10
- package/dist/agent/fakeAgent.js +11 -7
- package/dist/agent/piAdapter.d.ts +2 -1
- package/dist/agent/piAdapter.js +28 -25
- package/dist/agent/port.d.ts +6 -1
- package/dist/agent/port.js +3 -0
- package/dist/cli/artifactCommand.d.ts +10 -0
- package/dist/cli/artifactCommand.js +120 -0
- package/dist/cli/envelopeCommand.d.ts +32 -0
- package/dist/cli/envelopeCommand.js +285 -0
- package/dist/cli/exportRunCommand.d.ts +10 -0
- package/dist/cli/exportRunCommand.js +150 -0
- package/dist/cli/handoffFormat.d.ts +22 -0
- package/dist/cli/handoffFormat.js +61 -0
- package/dist/cli/runCommand.d.ts +5 -1
- package/dist/cli/runCommand.js +36 -5
- package/dist/cli/runOutput.d.ts +4 -1
- package/dist/cli/runOutput.js +36 -8
- package/dist/cli/skillsCommand.d.ts +16 -0
- package/dist/cli/skillsCommand.js +459 -0
- package/dist/cli.js +78 -5
- package/dist/config/loadPipeline.js +5 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
- package/dist/config/normalizePipelineStageEntry.js +25 -1
- package/dist/config/pipelineStageKeys.js +1 -1
- package/dist/config/resolveForkEmitContext.d.ts +2 -1
- package/dist/config/resolveForkEmitContext.js +23 -1
- package/dist/config/resolvePipelineDag.js +36 -4
- package/dist/envelope/check.d.ts +2 -0
- package/dist/envelope/check.js +51 -0
- package/dist/envelope/cloneForks.d.ts +2 -0
- package/dist/envelope/cloneForks.js +62 -0
- package/dist/envelope/forkChoice.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/catalogTools.d.ts +3 -0
- package/dist/mcp/catalogTools.js +187 -0
- package/dist/mcp/controlTools.d.ts +3 -0
- package/dist/mcp/controlTools.js +147 -0
- package/dist/mcp/deps.d.ts +11 -0
- package/dist/mcp/deps.js +1 -0
- package/dist/mcp/projectRun.d.ts +1 -40
- package/dist/mcp/projectRun.js +1 -64
- package/dist/mcp/resources.d.ts +5 -0
- package/dist/mcp/resources.js +45 -0
- package/dist/mcp/server.d.ts +16 -3
- package/dist/mcp/server.js +159 -3
- package/dist/mcp/toolResults.d.ts +7 -0
- package/dist/mcp/toolResults.js +6 -0
- package/dist/mcp/tools.d.ts +2 -7
- package/dist/mcp/tools.js +52 -103
- package/dist/mcp/waitRun.d.ts +46 -0
- package/dist/mcp/waitRun.js +146 -0
- package/dist/mcp/waitingGates.d.ts +12 -0
- package/dist/mcp/waitingGates.js +83 -0
- package/dist/projection/projectRun.d.ts +44 -0
- package/dist/projection/projectRun.js +71 -0
- package/dist/prompt/priorEnvelope.d.ts +1 -1
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/paths.js +2 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
- package/dist/runstore/pipelineDagSnapshot.js +90 -1
- package/dist/runstore/port.d.ts +13 -2
- package/dist/runstore/port.js +4 -1
- package/dist/runstore/runProjection.d.ts +1 -1
- package/dist/runstore/runProjection.js +14 -3
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
- package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
- package/dist/runstore/stageInstanceId.d.ts +4 -0
- package/dist/runstore/stageInstanceId.js +23 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
- package/dist/runstore/syntheticStageSnapshot.js +2 -1
- package/dist/runstore/trackProjection.js +28 -12
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +1 -1
- package/dist/runtime/cloneSchedule.d.ts +18 -0
- package/dist/runtime/cloneSchedule.js +233 -0
- package/dist/runtime/envelopeRouting.d.ts +2 -1
- package/dist/runtime/envelopeRouting.js +80 -13
- package/dist/runtime/pipelineScheduler.d.ts +6 -2
- package/dist/runtime/pipelineScheduler.js +166 -30
- package/dist/runtime/resumeReconstruct.js +8 -3
- package/dist/runtime/runChangeBus.d.ts +18 -0
- package/dist/runtime/runChangeBus.js +83 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
- package/dist/runtime/stageAttemptBootstrap.js +17 -9
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +17 -15
- package/dist/runtime/stageWorker.js +11 -4
- package/dist/server/bootstrap.d.ts +32 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/createHttpHost.d.ts +34 -0
- package/dist/server/createHttpHost.js +70 -0
- package/dist/server/http.d.ts +6 -13
- package/dist/server/http.js +295 -374
- package/dist/server/mcpHost.d.ts +22 -0
- package/dist/server/mcpHost.js +21 -0
- package/dist/server/operatorResults.d.ts +13 -0
- package/dist/server/operatorResults.js +22 -0
- package/dist/tools/emitStageEnvelope.d.ts +11 -2
- package/dist/tools/emitStageEnvelope.js +22 -6
- package/dist/types/envelope.d.ts +2 -0
- package/dist/types/forkChoice.d.ts +22 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +118 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/install-suite.sh +107 -0
- package/skills/stageflow/SKILL.md +26 -0
- package/skills/stageflow/references/control-surface.md +26 -0
- package/skills/stageflow/scripts/detect-host.mjs +54 -0
- package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
- package/skills/stageflow-author/SKILL.md +58 -0
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
- package/skills/stageflow-author/references/catalog-mapping.md +131 -0
- package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
- package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
- package/skills/stageflow-author/references/validate-and-report.md +38 -0
- package/skills/stageflow-delegate/SKILL.md +18 -0
- package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
- package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
- package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
- package/skills/stageflow-run/SKILL.md +151 -0
- package/skills/stageflow-run/references/mcp-call.md +17 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
- package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
- package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
- package/skills/stageflow-session-capture/SKILL.md +94 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
- package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
- package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
- package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
- package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
- package/skills/stageflow-setup/SKILL.md +110 -0
- package/dist/ui/assets/index-CFSzDZje.js +0 -118
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { normalizeForkChoice } from "../envelope/forkChoice.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
|
|
3
|
+
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
4
|
+
import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, cloneFailureContinuesSchedule, cloneFanoutConflict, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, isCloneInstance, protectedClonableChildIds, sequentialLaterCloneIds, } from "./cloneSchedule.js";
|
|
5
|
+
import { collectDownstreamStageIds } from "./dagTraversal.js";
|
|
3
6
|
import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
|
|
4
7
|
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
5
8
|
import { runStage, isRunStageWaiting } from "./stageRunner.js";
|
|
6
9
|
import { attemptContext } from "./stageAttemptContext.js";
|
|
10
|
+
function asDagSnapshot(dag) {
|
|
11
|
+
const snapshot = dag;
|
|
12
|
+
if (Array.isArray(snapshot.stage_ids))
|
|
13
|
+
return snapshot;
|
|
14
|
+
return {
|
|
15
|
+
...dag,
|
|
16
|
+
stage_ids: dag.nodes.map((n) => n.id),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, };
|
|
20
|
+
function synthesizedFailureEnvelope(reason) {
|
|
21
|
+
return { status: "failure", summary: reason, artifacts: [] };
|
|
22
|
+
}
|
|
7
23
|
export function snapshotToScheduleState(snap, overrides, stageId, executionMode) {
|
|
8
24
|
const override = overrides.get(stageId);
|
|
9
25
|
if (override !== undefined)
|
|
@@ -17,23 +33,28 @@ export function snapshotToScheduleState(snap, overrides, stageId, executionMode)
|
|
|
17
33
|
return "active";
|
|
18
34
|
case "waiting_for_input":
|
|
19
35
|
return executionMode === "process" ? "waiting" : "active";
|
|
36
|
+
case "skipped":
|
|
37
|
+
return "skipped";
|
|
20
38
|
default:
|
|
21
39
|
return "pending";
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
export async function hydrateScheduleFromStore(store, runId, dag, executionMode = "process") {
|
|
43
|
+
const runDetail = await store.readRun(runId);
|
|
44
|
+
const meta = await store.readRunMeta(runId);
|
|
45
|
+
const resolvedDag = meta.pipeline_dag ?? asDagSnapshot(dag);
|
|
25
46
|
const states = new Map();
|
|
26
|
-
for (const node of
|
|
47
|
+
for (const node of resolvedDag.nodes) {
|
|
27
48
|
states.set(node.id, "pending");
|
|
28
49
|
}
|
|
29
|
-
const
|
|
30
|
-
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
|
|
50
|
+
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages, resolvedDag);
|
|
31
51
|
let schedulingHalted = false;
|
|
32
52
|
const overrides = new Map();
|
|
33
53
|
for (const snap of runDetail.stages) {
|
|
34
54
|
const state = snapshotToScheduleState(snap, overrides, snap.stage_id, executionMode);
|
|
35
55
|
states.set(snap.stage_id, state);
|
|
36
|
-
if (state === "failed"
|
|
56
|
+
if (state === "failed" &&
|
|
57
|
+
!cloneFailureContinuesSchedule(resolvedDag, snap.stage_id, completedEnvelopes)) {
|
|
37
58
|
schedulingHalted = true;
|
|
38
59
|
}
|
|
39
60
|
}
|
|
@@ -42,15 +63,23 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
|
|
|
42
63
|
completedEnvelopes,
|
|
43
64
|
schedulingHalted,
|
|
44
65
|
firstFailureReason: undefined,
|
|
66
|
+
dag: resolvedDag,
|
|
45
67
|
};
|
|
46
68
|
}
|
|
47
69
|
export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStageIds) {
|
|
48
70
|
const runDetail = await store.readRun(runId);
|
|
49
|
-
const
|
|
71
|
+
const meta = await store.readRunMeta(runId);
|
|
72
|
+
const resolvedDag = meta.pipeline_dag ?? asDagSnapshot(dag);
|
|
73
|
+
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages, resolvedDag);
|
|
74
|
+
const downstream = cloneRetryDownstream(resolvedDag, retryStageIds);
|
|
75
|
+
for (const rootId of retryStageIds) {
|
|
76
|
+
for (const id of sequentialLaterCloneIds(resolvedDag, rootId, completedEnvelopes)) {
|
|
77
|
+
downstream.add(id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
50
80
|
const retryRootSet = new Set(retryStageIds);
|
|
51
|
-
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
|
|
52
81
|
const states = new Map();
|
|
53
|
-
for (const node of
|
|
82
|
+
for (const node of resolvedDag.nodes) {
|
|
54
83
|
states.set(node.id, "pending");
|
|
55
84
|
}
|
|
56
85
|
const overrides = new Map();
|
|
@@ -73,6 +102,7 @@ export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStage
|
|
|
73
102
|
completedEnvelopes,
|
|
74
103
|
schedulingHalted: false,
|
|
75
104
|
firstFailureReason: undefined,
|
|
105
|
+
dag: resolvedDag,
|
|
76
106
|
};
|
|
77
107
|
}
|
|
78
108
|
export async function hydrateScheduleForRetry(store, runId, dag, retryStageId) {
|
|
@@ -80,7 +110,8 @@ export async function hydrateScheduleForRetry(store, runId, dag, retryStageId) {
|
|
|
80
110
|
}
|
|
81
111
|
export async function resumeRun(options) {
|
|
82
112
|
const { prepared, resumeFromStageId, initialPrior } = options;
|
|
83
|
-
const
|
|
113
|
+
const meta = await prepared.store.readRunMeta(prepared.run.runId);
|
|
114
|
+
const dag = meta.pipeline_dag ?? buildPipelineDagSnapshotFromLoaded(prepared.loaded);
|
|
84
115
|
const downstream = dag.childrenOf[resumeFromStageId];
|
|
85
116
|
if (downstream?.length) {
|
|
86
117
|
return runPipelineDag(options);
|
|
@@ -124,7 +155,7 @@ export async function resumeRun(options) {
|
|
|
124
155
|
}
|
|
125
156
|
export async function retryRun(options) {
|
|
126
157
|
const { prepared, retryRoots } = options;
|
|
127
|
-
const hydrated = await hydrateScheduleForRetryRoots(prepared.store, prepared.run.runId, prepared.loaded
|
|
158
|
+
const hydrated = await hydrateScheduleForRetryRoots(prepared.store, prepared.run.runId, buildPipelineDagSnapshotFromLoaded(prepared.loaded), [...retryRoots.keys()]);
|
|
128
159
|
return runPipelineDag({
|
|
129
160
|
prepared: options.prepared,
|
|
130
161
|
maxActiveStagesPerRun: options.maxActiveStagesPerRun,
|
|
@@ -145,6 +176,8 @@ function applyForkChoiceToSchedule(dag, forkStageId, chosen, states, options) {
|
|
|
145
176
|
}
|
|
146
177
|
};
|
|
147
178
|
for (const childId of dag.childrenOf[forkStageId] ?? []) {
|
|
179
|
+
if (options?.protectedIds?.has(childId))
|
|
180
|
+
continue;
|
|
148
181
|
if (!chosen.has(childId)) {
|
|
149
182
|
skipPending(childId);
|
|
150
183
|
for (const desc of collectDownstreamStageIds(dag, childId)) {
|
|
@@ -166,8 +199,11 @@ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
|
|
|
166
199
|
}
|
|
167
200
|
export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt) {
|
|
168
201
|
retryRoots.set(stageId, attempt);
|
|
169
|
-
const downstream =
|
|
202
|
+
const downstream = cloneRetryDownstream(dag, [stageId]);
|
|
170
203
|
const resetIds = new Set([stageId, ...downstream]);
|
|
204
|
+
for (const id of sequentialLaterCloneIds(dag, stageId, completedEnvelopes)) {
|
|
205
|
+
resetIds.add(id);
|
|
206
|
+
}
|
|
171
207
|
for (const id of resetIds) {
|
|
172
208
|
const state = states.get(id);
|
|
173
209
|
if (state === "active" || state === "succeeded") {
|
|
@@ -221,28 +257,28 @@ function sortRunnableIds(dag, ids) {
|
|
|
221
257
|
const indexById = new Map(dag.nodes.map((node) => [node.id, node.stageIndex]));
|
|
222
258
|
return ids.slice().sort((a, b) => (indexById.get(a) ?? 0) - (indexById.get(b) ?? 0));
|
|
223
259
|
}
|
|
224
|
-
function isRunnable(dag, stageId, states, schedulingHalted) {
|
|
260
|
+
function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes) {
|
|
225
261
|
if (schedulingHalted)
|
|
226
262
|
return false;
|
|
227
263
|
const state = states.get(stageId);
|
|
228
264
|
if (state !== "pending")
|
|
229
265
|
return false;
|
|
230
|
-
|
|
231
|
-
if (!node)
|
|
232
|
-
return false;
|
|
233
|
-
if (node.needs === null)
|
|
234
|
-
return true;
|
|
235
|
-
return states.get(node.needs) === "succeeded";
|
|
266
|
+
return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes);
|
|
236
267
|
}
|
|
237
268
|
export async function runPipelineDag(options) {
|
|
238
269
|
const { prepared, maxActiveStagesPerRun } = options;
|
|
239
270
|
const executionMode = options.executionMode ?? "process";
|
|
240
271
|
const { agent, task, loaded, run, store, checkoutRoot, hitl, cwd, projectRoot, } = prepared;
|
|
241
272
|
const factoryCwd = projectRoot ?? cwd ?? process.cwd();
|
|
242
|
-
const
|
|
273
|
+
const fallbackDag = buildPipelineDagSnapshotFromLoaded(loaded);
|
|
274
|
+
let { states, completedEnvelopes, schedulingHalted, firstFailureReason, dag: hydratedDag, } = options.initialSchedule ??
|
|
275
|
+
(await hydrateScheduleFromStore(store, run.runId, fallbackDag, executionMode));
|
|
276
|
+
let dag = hydratedDag ?? fallbackDag;
|
|
277
|
+
if (hydratedDag === undefined) {
|
|
278
|
+
const meta = await store.readRunMeta(run.runId);
|
|
279
|
+
dag = meta.pipeline_dag ?? fallbackDag;
|
|
280
|
+
}
|
|
243
281
|
const stageById = buildStageConfigById(loaded);
|
|
244
|
-
let { states, completedEnvelopes, schedulingHalted, firstFailureReason } = options.initialSchedule ??
|
|
245
|
-
(await hydrateScheduleFromStore(store, run.runId, dag, executionMode));
|
|
246
282
|
const retryContext = options.retryContext;
|
|
247
283
|
const resolveLaunchAttempt = async (stageId) => {
|
|
248
284
|
if (retryContext) {
|
|
@@ -293,6 +329,13 @@ export async function runPipelineDag(options) {
|
|
|
293
329
|
}
|
|
294
330
|
}
|
|
295
331
|
applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes);
|
|
332
|
+
{
|
|
333
|
+
const applied = applyCloneForksFromEnvelopes(dag, states, completedEnvelopes);
|
|
334
|
+
if (applied.dag !== dag) {
|
|
335
|
+
await store.updatePipelineDag(run.runId, applied.dag);
|
|
336
|
+
}
|
|
337
|
+
dag = applied.dag;
|
|
338
|
+
}
|
|
296
339
|
let activeCount = 0;
|
|
297
340
|
const inFlight = new Set();
|
|
298
341
|
const notifyRetryRootTerminal = (stageId, outcome) => {
|
|
@@ -303,6 +346,20 @@ export async function runPipelineDag(options) {
|
|
|
303
346
|
return;
|
|
304
347
|
options.onRetryRootTerminal?.(stageId, attempt, outcome);
|
|
305
348
|
};
|
|
349
|
+
const persistSkipPending = async (id) => {
|
|
350
|
+
if (states.get(id) !== "pending")
|
|
351
|
+
return;
|
|
352
|
+
states.set(id, "skipped");
|
|
353
|
+
notifyRetryRootTerminal(id, "skipped");
|
|
354
|
+
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
355
|
+
};
|
|
356
|
+
for (const [stageId, state] of states) {
|
|
357
|
+
if (state !== "failed")
|
|
358
|
+
continue;
|
|
359
|
+
for (const id of cloneFailFastSkipIds(dag, stageId, completedEnvelopes)) {
|
|
360
|
+
await persistSkipPending(id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
306
363
|
const markSkippedPending = () => {
|
|
307
364
|
for (const node of dag.nodes) {
|
|
308
365
|
if (states.get(node.id) === "pending") {
|
|
@@ -333,9 +390,41 @@ export async function runPipelineDag(options) {
|
|
|
333
390
|
});
|
|
334
391
|
});
|
|
335
392
|
};
|
|
336
|
-
const
|
|
393
|
+
const recordCloneFailureEnvelope = async (stageId, reason, envelope) => {
|
|
394
|
+
if (!isCloneInstance(dag, stageId)) {
|
|
395
|
+
if (envelope !== undefined) {
|
|
396
|
+
completedEnvelopes.set(stageId, envelope);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const failureEnvelope = envelope ??
|
|
401
|
+
completedEnvelopes.get(stageId) ??
|
|
402
|
+
synthesizedFailureEnvelope(reason);
|
|
403
|
+
completedEnvelopes.set(stageId, failureEnvelope);
|
|
404
|
+
if (envelope === undefined) {
|
|
405
|
+
try {
|
|
406
|
+
await store.writeEnvelope(run.runId, stageId, failureEnvelope);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const onStageFailure = async (stageId, reason, envelope) => {
|
|
337
413
|
states.set(stageId, "failed");
|
|
338
414
|
notifyRetryRootTerminal(stageId, "failed");
|
|
415
|
+
await recordCloneFailureEnvelope(stageId, reason, envelope);
|
|
416
|
+
if (cloneFailureContinuesSchedule(dag, stageId, completedEnvelopes)) {
|
|
417
|
+
if (firstFailureReason === undefined) {
|
|
418
|
+
firstFailureReason = reason;
|
|
419
|
+
}
|
|
420
|
+
for (const id of cloneFailFastSkipIds(dag, stageId, completedEnvelopes)) {
|
|
421
|
+
await persistSkipPending(id);
|
|
422
|
+
}
|
|
423
|
+
if (retryContext) {
|
|
424
|
+
drainRetryMutations();
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
339
428
|
if (retryContext) {
|
|
340
429
|
markBranchDownstreamSkipped(stageId);
|
|
341
430
|
drainRetryMutations();
|
|
@@ -352,20 +441,59 @@ export async function runPipelineDag(options) {
|
|
|
352
441
|
firstFailureReason = reason;
|
|
353
442
|
}
|
|
354
443
|
};
|
|
355
|
-
const onStageSuccess = (stageId, envelope) => {
|
|
444
|
+
const onStageSuccess = async (stageId, envelope) => {
|
|
445
|
+
const conflict = cloneFanoutConflict(dag, envelope);
|
|
446
|
+
if (conflict !== undefined) {
|
|
447
|
+
await store.appendStageEvent(run.runId, stageId, {
|
|
448
|
+
event: "failed",
|
|
449
|
+
reason: conflict,
|
|
450
|
+
});
|
|
451
|
+
await onStageFailure(stageId, conflict);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
356
454
|
states.set(stageId, "succeeded");
|
|
357
455
|
completedEnvelopes.set(stageId, envelope);
|
|
358
456
|
notifyRetryRootTerminal(stageId, "succeeded");
|
|
359
457
|
const node = dag.nodes.find((n) => n.id === stageId);
|
|
458
|
+
const protectedIds = protectedClonableChildIds(dag, stageId, envelope);
|
|
360
459
|
if (node?.fork) {
|
|
361
460
|
const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
|
|
461
|
+
const skippedIds = [];
|
|
362
462
|
applyForkChoiceToSchedule(dag, stageId, chosen, states, {
|
|
363
|
-
|
|
463
|
+
protectedIds,
|
|
464
|
+
onSkip: (id) => {
|
|
465
|
+
notifyRetryRootTerminal(id, "skipped");
|
|
466
|
+
skippedIds.push(id);
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
for (const id of skippedIds) {
|
|
470
|
+
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const applied = applyCloneForksToSchedule(dag, stageId, envelope, states);
|
|
474
|
+
const dagChanged = applied.dag !== dag;
|
|
475
|
+
dag = applied.dag;
|
|
476
|
+
if (dagChanged) {
|
|
477
|
+
await store.updatePipelineDag(run.runId, dag);
|
|
478
|
+
}
|
|
479
|
+
for (const id of applied.skippedIds) {
|
|
480
|
+
notifyRetryRootTerminal(id, "skipped");
|
|
481
|
+
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const handlePostSuccessError = async (stageId, reason) => {
|
|
485
|
+
if (states.get(stageId) === "succeeded") {
|
|
486
|
+
completedEnvelopes.delete(stageId);
|
|
487
|
+
await store.appendStageEvent(run.runId, stageId, {
|
|
488
|
+
event: "failed",
|
|
489
|
+
reason,
|
|
364
490
|
});
|
|
365
491
|
}
|
|
492
|
+
await onStageFailure(stageId, reason);
|
|
366
493
|
};
|
|
367
494
|
const launchStage = async (stageId) => {
|
|
368
|
-
const
|
|
495
|
+
const definitionId = definitionIdForInstance(dag, stageId);
|
|
496
|
+
const stage = stageById.get(definitionId);
|
|
369
497
|
if (!stage) {
|
|
370
498
|
await onStageFailure(stageId, `stage config missing for "${stageId}"`);
|
|
371
499
|
return;
|
|
@@ -391,10 +519,11 @@ export async function runPipelineDag(options) {
|
|
|
391
519
|
if (launchResult.type === "succeeded") {
|
|
392
520
|
try {
|
|
393
521
|
const envelope = await store.readEnvelope(run.runId, stageId);
|
|
394
|
-
onStageSuccess(stageId, envelope);
|
|
522
|
+
await onStageSuccess(stageId, envelope);
|
|
395
523
|
}
|
|
396
524
|
catch (err) {
|
|
397
|
-
|
|
525
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
526
|
+
await handlePostSuccessError(stageId, reason);
|
|
398
527
|
}
|
|
399
528
|
return;
|
|
400
529
|
}
|
|
@@ -410,6 +539,7 @@ export async function runPipelineDag(options) {
|
|
|
410
539
|
store,
|
|
411
540
|
runId: run.runId,
|
|
412
541
|
stage,
|
|
542
|
+
stageId,
|
|
413
543
|
task,
|
|
414
544
|
dag,
|
|
415
545
|
checkoutRoot,
|
|
@@ -426,11 +556,17 @@ export async function runPipelineDag(options) {
|
|
|
426
556
|
return;
|
|
427
557
|
}
|
|
428
558
|
if (!result.ok) {
|
|
429
|
-
await onStageFailure(stageId, result.reason ?? "stage failed");
|
|
559
|
+
await onStageFailure(stageId, result.reason ?? "stage failed", result.envelope);
|
|
430
560
|
return;
|
|
431
561
|
}
|
|
432
562
|
if (result.envelope) {
|
|
433
|
-
|
|
563
|
+
try {
|
|
564
|
+
await onStageSuccess(stageId, result.envelope);
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
568
|
+
await handlePostSuccessError(stageId, reason);
|
|
569
|
+
}
|
|
434
570
|
}
|
|
435
571
|
else {
|
|
436
572
|
states.set(stageId, "succeeded");
|
|
@@ -451,7 +587,7 @@ export async function runPipelineDag(options) {
|
|
|
451
587
|
const pickRunnable = () => {
|
|
452
588
|
const ids = [];
|
|
453
589
|
for (const node of dag.nodes) {
|
|
454
|
-
if (isRunnable(dag, node.id, states, schedulingHalted)) {
|
|
590
|
+
if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes)) {
|
|
455
591
|
ids.push(node.id);
|
|
456
592
|
}
|
|
457
593
|
}
|
|
@@ -4,6 +4,7 @@ import { fakeHitlResumePath } from "../agent/fakeAgent.js";
|
|
|
4
4
|
import { loadTaskFromYaml } from "../config/loadTask.js";
|
|
5
5
|
import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
|
|
6
6
|
import { reloadPipelineForRun, reloadTaskForRun, } from "./reloadRunCatalog.js";
|
|
7
|
+
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
7
8
|
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
8
9
|
import { resumeRun } from "./pipelineScheduler.js";
|
|
9
10
|
import { buildStageRoots, withResolvedAuthPath } from "./stageRoots.js";
|
|
@@ -38,7 +39,8 @@ export async function reconstructAndContinue(ctx) {
|
|
|
38
39
|
const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
|
|
39
40
|
const attemptOpt = attemptCtx.eventOptions();
|
|
40
41
|
const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
|
|
41
|
-
const
|
|
42
|
+
const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
|
|
43
|
+
const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
|
|
42
44
|
if (stageIndex < 0) {
|
|
43
45
|
const reason = `Stage ${stageId} not in pipeline ${meta.pipeline_id}`;
|
|
44
46
|
await store.appendStageEvent(runId, stageId, {
|
|
@@ -49,6 +51,7 @@ export async function reconstructAndContinue(ctx) {
|
|
|
49
51
|
return { ok: false, reason };
|
|
50
52
|
}
|
|
51
53
|
const stage = loaded.stages[stageIndex];
|
|
54
|
+
const dag = meta.pipeline_dag ?? loaded.dag;
|
|
52
55
|
const workspaceDir = store.getWorkspaceDir(runId);
|
|
53
56
|
const checkoutRoot = meta.checkout_root;
|
|
54
57
|
const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), factoryCwd);
|
|
@@ -68,8 +71,9 @@ export async function reconstructAndContinue(ctx) {
|
|
|
68
71
|
store,
|
|
69
72
|
runId,
|
|
70
73
|
stage,
|
|
74
|
+
stageId,
|
|
71
75
|
task,
|
|
72
|
-
dag
|
|
76
|
+
dag,
|
|
73
77
|
checkoutRoot,
|
|
74
78
|
workspaceDir,
|
|
75
79
|
factoryCwd,
|
|
@@ -93,8 +97,9 @@ export async function reconstructAndContinue(ctx) {
|
|
|
93
97
|
store,
|
|
94
98
|
runId,
|
|
95
99
|
stage,
|
|
100
|
+
stageId,
|
|
96
101
|
task,
|
|
97
|
-
dag
|
|
102
|
+
dag,
|
|
98
103
|
checkoutRoot,
|
|
99
104
|
workspaceDir,
|
|
100
105
|
hitl: ctx.hitl,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
2
|
+
export type RunChangeKind = "created" | "status" | "waiting";
|
|
3
|
+
export type RunChangeEvent = {
|
|
4
|
+
runId: string;
|
|
5
|
+
kind: RunChangeKind;
|
|
6
|
+
};
|
|
7
|
+
type RunChangeListener = (event: RunChangeEvent) => void;
|
|
8
|
+
export declare class RunChangeBus {
|
|
9
|
+
private readonly listeners;
|
|
10
|
+
on(listener: RunChangeListener): () => void;
|
|
11
|
+
off(listener: RunChangeListener): void;
|
|
12
|
+
emit(event: RunChangeEvent): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRunChangeBus(): RunChangeBus;
|
|
15
|
+
export declare function isRunStoreWrapped(store: RunStore): boolean;
|
|
16
|
+
export declare function getRunChangeBusFromWrappedStore(store: RunStore): RunChangeBus | undefined;
|
|
17
|
+
export declare function wrapRunStoreWithChangeBus(store: RunStore, bus: RunChangeBus): RunStore;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export class RunChangeBus {
|
|
2
|
+
listeners = new Set();
|
|
3
|
+
on(listener) {
|
|
4
|
+
this.listeners.add(listener);
|
|
5
|
+
return () => {
|
|
6
|
+
this.listeners.delete(listener);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
off(listener) {
|
|
10
|
+
this.listeners.delete(listener);
|
|
11
|
+
}
|
|
12
|
+
emit(event) {
|
|
13
|
+
for (const listener of [...this.listeners]) {
|
|
14
|
+
try {
|
|
15
|
+
listener(event);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Listener isolation: one throw must not block others.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function createRunChangeBus() {
|
|
24
|
+
return new RunChangeBus();
|
|
25
|
+
}
|
|
26
|
+
const WRAPPED_MARKER = Symbol.for("stageflow.wrapRunStoreWithChangeBus");
|
|
27
|
+
const BUS_MARKER = Symbol.for("stageflow.runChangeBus");
|
|
28
|
+
export function isRunStoreWrapped(store) {
|
|
29
|
+
return store[WRAPPED_MARKER] === true;
|
|
30
|
+
}
|
|
31
|
+
export function getRunChangeBusFromWrappedStore(store) {
|
|
32
|
+
const bus = store[BUS_MARKER];
|
|
33
|
+
return bus instanceof RunChangeBus ? bus : undefined;
|
|
34
|
+
}
|
|
35
|
+
export function wrapRunStoreWithChangeBus(store, bus) {
|
|
36
|
+
if (isRunStoreWrapped(store)) {
|
|
37
|
+
const bound = getRunChangeBusFromWrappedStore(store);
|
|
38
|
+
if (bound !== bus) {
|
|
39
|
+
throw new Error("runChangeBus does not match the bus already bound to the provided store");
|
|
40
|
+
}
|
|
41
|
+
return store;
|
|
42
|
+
}
|
|
43
|
+
const createRun = async (input) => {
|
|
44
|
+
const created = await store.createRun(input);
|
|
45
|
+
bus.emit({ runId: created.runId, kind: "created" });
|
|
46
|
+
return created;
|
|
47
|
+
};
|
|
48
|
+
const updateRunStatus = async (runId, status) => {
|
|
49
|
+
await store.updateRunStatus(runId, status);
|
|
50
|
+
bus.emit({ runId, kind: "status" });
|
|
51
|
+
};
|
|
52
|
+
const appendStageEvent = async (runId, stageId, event, options) => {
|
|
53
|
+
await store.appendStageEvent(runId, stageId, event, options);
|
|
54
|
+
if (event.event === "waiting_for_input" || event.event === "resumed") {
|
|
55
|
+
bus.emit({ runId, kind: "waiting" });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const boundCache = new Map();
|
|
59
|
+
return new Proxy(store, {
|
|
60
|
+
get(target, prop, receiver) {
|
|
61
|
+
if (prop === WRAPPED_MARKER)
|
|
62
|
+
return true;
|
|
63
|
+
if (prop === BUS_MARKER)
|
|
64
|
+
return bus;
|
|
65
|
+
if (prop === "createRun")
|
|
66
|
+
return createRun;
|
|
67
|
+
if (prop === "updateRunStatus")
|
|
68
|
+
return updateRunStatus;
|
|
69
|
+
if (prop === "appendStageEvent")
|
|
70
|
+
return appendStageEvent;
|
|
71
|
+
const cached = boundCache.get(prop);
|
|
72
|
+
if (cached !== undefined)
|
|
73
|
+
return cached;
|
|
74
|
+
const value = Reflect.get(target, prop, receiver);
|
|
75
|
+
if (typeof value === "function") {
|
|
76
|
+
const bound = value.bind(target);
|
|
77
|
+
boundCache.set(prop, bound);
|
|
78
|
+
return bound;
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveSkillByName } from "../config/listSkills.js";
|
|
2
|
-
import { resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
|
|
2
|
+
import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
|
|
3
3
|
import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
|
|
4
4
|
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
5
5
|
import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
|
|
@@ -39,40 +39,48 @@ async function openStageWithOperatorCatalog(agent, input, catalog) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
function resolveAttemptRoots(input) {
|
|
42
|
+
function resolveAttemptRoots(input, stageId) {
|
|
43
43
|
const baseRoots = input.roots ??
|
|
44
44
|
(input.workerRoots
|
|
45
|
-
? rootsForStageWorker(input.workspaceDir,
|
|
46
|
-
: buildStageRoots(input.workspaceDir,
|
|
45
|
+
? rootsForStageWorker(input.workspaceDir, stageId, input.stage.model, input.checkoutRoot, input.attemptCtx)
|
|
46
|
+
: buildStageRoots(input.workspaceDir, stageId, input.checkoutRoot, input.attemptCtx));
|
|
47
47
|
return input.factoryCwd !== undefined
|
|
48
48
|
? withResolvedAuthPath(baseRoots, input.factoryCwd)
|
|
49
49
|
: baseRoots;
|
|
50
50
|
}
|
|
51
51
|
export async function openStageAttempt(input) {
|
|
52
|
+
const stageId = input.stageId ?? input.stage.id;
|
|
53
|
+
const definitionId = input.stage.id;
|
|
52
54
|
const completedEnvelopes = input.completedEnvelopes ??
|
|
53
|
-
(await buildCompletedEnvelopesFromRun(input.store, input.runId));
|
|
55
|
+
(await buildCompletedEnvelopesFromRun(input.store, input.runId, undefined, input.dag));
|
|
54
56
|
const priorResult = await resolvePriorEnvelope({
|
|
55
57
|
dag: input.dag,
|
|
56
|
-
stageId
|
|
58
|
+
stageId,
|
|
57
59
|
completedEnvelopes,
|
|
58
60
|
store: input.store,
|
|
59
61
|
runId: input.runId,
|
|
60
62
|
});
|
|
61
63
|
if (!priorResult.ok)
|
|
62
64
|
return priorResult;
|
|
63
|
-
const roots = resolveAttemptRoots(input);
|
|
65
|
+
const roots = resolveAttemptRoots(input, stageId);
|
|
64
66
|
const attempt = input.attemptCtx?.attempt ?? 1;
|
|
65
67
|
const resumeToken = input.resumeToken ??
|
|
66
|
-
resumeSessionFilePath(input.workspaceDir,
|
|
67
|
-
const forkEmitContext = resolveForkEmitContext(input.dag,
|
|
68
|
+
resumeSessionFilePath(input.workspaceDir, stageId, attempt);
|
|
69
|
+
const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
|
|
70
|
+
const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId);
|
|
68
71
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
69
72
|
roots,
|
|
70
73
|
stage: input.stage,
|
|
74
|
+
stageId,
|
|
71
75
|
task: input.task,
|
|
72
76
|
priorEnvelope: priorResult.prior,
|
|
77
|
+
...(priorResult.joinPriors !== undefined
|
|
78
|
+
? { priorEnvelopes: priorResult.joinPriors }
|
|
79
|
+
: {}),
|
|
73
80
|
resumeToken,
|
|
74
81
|
onActivity: input.onActivity,
|
|
75
82
|
forkEmitContext,
|
|
83
|
+
cloneEmitContext,
|
|
76
84
|
}, input.operatorCatalog);
|
|
77
85
|
if (!opened.ok)
|
|
78
86
|
return opened;
|