stageflow 0.15.0 → 0.18.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.
Files changed (114) hide show
  1. package/README.md +2 -4
  2. package/dist/agent/activity.d.ts +3 -0
  3. package/dist/agent/claudeAdapter.js +84 -7
  4. package/dist/agent/claudeSession.d.ts +3 -0
  5. package/dist/agent/claudeTools.d.ts +1 -4
  6. package/dist/agent/claudeTools.js +1 -6
  7. package/dist/agent/fakeAgent.js +4 -5
  8. package/dist/agent/piAdapter.d.ts +2 -0
  9. package/dist/agent/piAdapter.js +30 -19
  10. package/dist/agent/port.d.ts +3 -0
  11. package/dist/cli/runOutput.js +32 -8
  12. package/dist/config/cloneChain.d.ts +12 -0
  13. package/dist/config/cloneChain.js +322 -0
  14. package/dist/config/createPipeline.js +101 -21
  15. package/dist/config/createStage.js +7 -0
  16. package/dist/config/loadOutcome.d.ts +1 -0
  17. package/dist/config/loadPipeline.d.ts +2 -0
  18. package/dist/config/loadPipeline.js +34 -22
  19. package/dist/config/loadStage.d.ts +2 -0
  20. package/dist/config/loadStage.js +135 -125
  21. package/dist/config/migrateYaml.js +14 -3
  22. package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
  23. package/dist/config/normalizePipelineStageEntry.js +133 -60
  24. package/dist/config/parsePreEmitChecks.js +2 -2
  25. package/dist/config/pipelineNeeds.d.ts +15 -2
  26. package/dist/config/pipelineNeeds.js +86 -4
  27. package/dist/config/pipelineRoute.d.ts +46 -0
  28. package/dist/config/pipelineRoute.js +215 -0
  29. package/dist/config/pipelineStageKeys.d.ts +2 -0
  30. package/dist/config/pipelineStageKeys.js +15 -4
  31. package/dist/config/printTargetYaml.js +18 -12
  32. package/dist/config/resolveForkEmitContext.d.ts +1 -1
  33. package/dist/config/resolveForkEmitContext.js +3 -26
  34. package/dist/config/resolvePipelineDag.d.ts +1 -3
  35. package/dist/config/resolvePipelineDag.js +143 -186
  36. package/dist/config/routeIf.d.ts +24 -0
  37. package/dist/config/routeIf.js +247 -0
  38. package/dist/config/validateCatalog.d.ts +1 -1
  39. package/dist/config/validateCatalog.js +5 -3
  40. package/dist/config/yamlDialect.d.ts +5 -8
  41. package/dist/config/yamlDialect.js +37 -29
  42. package/dist/envelope/check.d.ts +0 -2
  43. package/dist/envelope/check.js +1 -57
  44. package/dist/envelope/feedbackLoop.js +0 -3
  45. package/dist/envelope/forkChoice.js +1 -1
  46. package/dist/envelope/payloadSchema.d.ts +14 -0
  47. package/dist/envelope/payloadSchema.js +87 -2
  48. package/dist/mcp/catalogTools.js +1 -2
  49. package/dist/package-meta.d.ts +1 -1
  50. package/dist/package-meta.js +1 -1
  51. package/dist/projection/projectRun.d.ts +0 -2
  52. package/dist/projection/projectRun.js +0 -3
  53. package/dist/prompt/priorEnvelope.js +1 -1
  54. package/dist/runstore/port.d.ts +10 -0
  55. package/dist/runstore/runProjection.js +9 -0
  56. package/dist/runstore/sqlite/SqliteRunStore.js +27 -4
  57. package/dist/runstore/sqlite/schema.d.ts +1 -1
  58. package/dist/runstore/sqlite/schema.js +2 -0
  59. package/dist/runstore/stageExecution.d.ts +3 -0
  60. package/dist/runstore/stageExecution.js +6 -2
  61. package/dist/runstore/stageInstanceId.d.ts +1 -0
  62. package/dist/runstore/stageInstanceId.js +9 -0
  63. package/dist/runstore/stageSnapshot.js +11 -0
  64. package/dist/runtime/cloneSchedule.d.ts +16 -33
  65. package/dist/runtime/cloneSchedule.js +155 -236
  66. package/dist/runtime/envelopeRouting.js +43 -94
  67. package/dist/runtime/forkGeneration.d.ts +2 -2
  68. package/dist/runtime/forkGeneration.js +29 -15
  69. package/dist/runtime/joinReadiness.d.ts +16 -0
  70. package/dist/runtime/joinReadiness.js +156 -0
  71. package/dist/runtime/pipelineScheduler.d.ts +0 -3
  72. package/dist/runtime/pipelineScheduler.js +81 -65
  73. package/dist/runtime/routeIfEval.d.ts +3 -0
  74. package/dist/runtime/routeIfEval.js +94 -0
  75. package/dist/runtime/runRetryCoordinator.js +0 -4
  76. package/dist/runtime/stageAttemptBootstrap.js +60 -11
  77. package/dist/runtime/stageRunner.js +2 -2
  78. package/dist/runtime/taskInput.js +1 -13
  79. package/dist/tools/emitStageEnvelope.d.ts +2 -3
  80. package/dist/tools/emitStageEnvelope.js +1 -65
  81. package/dist/types/envelope.d.ts +0 -2
  82. package/dist/types/forkChoice.d.ts +0 -16
  83. package/dist/types/pipeline.d.ts +86 -27
  84. package/dist/types/stage.d.ts +11 -7
  85. package/dist/types/usage.d.ts +16 -0
  86. package/dist/types/usage.js +24 -0
  87. package/dist/ui/assets/{index-uVHiae8s.css → index-DsgVjLUS.css} +1 -1
  88. package/dist/ui/assets/index-DwKGaQvQ.js +119 -0
  89. package/dist/ui/index.html +2 -2
  90. package/package.json +2 -1
  91. package/skills/stageflow-author/SKILL.md +4 -4
  92. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
  93. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
  94. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
  95. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
  96. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
  97. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
  98. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
  99. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
  100. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
  101. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
  102. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
  103. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
  104. package/skills/stageflow-author/references/catalog-mapping.md +159 -50
  105. package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
  106. package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
  107. package/skills/stageflow-session-capture/SKILL.md +2 -2
  108. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
  109. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
  110. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
  111. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
  112. package/dist/envelope/cloneForks.d.ts +0 -2
  113. package/dist/envelope/cloneForks.js +0 -77
  114. package/dist/ui/assets/index-GtyiWiZg.js +0 -118
@@ -4,7 +4,9 @@ import { MCP_CATALOG_FILENAME, STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV, StageMcpError,
4
4
  import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
5
5
  import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
6
6
  import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
7
+ import { assertPriorInputPayload, payloadInstanceMismatch, } from "../envelope/payloadSchema.js";
7
8
  import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
9
+ import { assignment } from "./cloneSchedule.js";
8
10
  import { resumeSessionFilePath } from "./stageAttemptContext.js";
9
11
  import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
10
12
  async function resolveStageSkillForRun(stage, catalog) {
@@ -76,6 +78,51 @@ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, a
76
78
  };
77
79
  }
78
80
  }
81
+ function assertPriorsMatchCloneInput(cloneInputSchema, childId, priorResult, task, skipCloneAssignment) {
82
+ if (cloneInputSchema === undefined)
83
+ return { ok: true };
84
+ if (skipCloneAssignment)
85
+ return { ok: true };
86
+ try {
87
+ if (priorResult.priorEnvelopesByStage !== undefined) {
88
+ for (const value of Object.values(priorResult.priorEnvelopesByStage)) {
89
+ const envelopes = Array.isArray(value) ? value : [value];
90
+ for (const envelope of envelopes) {
91
+ if (envelope.status !== "success")
92
+ continue;
93
+ assertPriorInputPayload(envelope, cloneInputSchema, childId);
94
+ }
95
+ }
96
+ return { ok: true };
97
+ }
98
+ if (priorResult.joinPriors !== undefined) {
99
+ for (const envelope of priorResult.joinPriors) {
100
+ if (envelope.status !== "success")
101
+ continue;
102
+ assertPriorInputPayload(envelope, cloneInputSchema, childId);
103
+ }
104
+ return { ok: true };
105
+ }
106
+ if (priorResult.prior !== null) {
107
+ assertPriorInputPayload(priorResult.prior, cloneInputSchema, childId);
108
+ return { ok: true };
109
+ }
110
+ const details = payloadInstanceMismatch(task.input ?? {}, cloneInputSchema);
111
+ if (details !== undefined) {
112
+ return {
113
+ ok: false,
114
+ reason: `task input does not match io.input.schema for ${childId}: ${details}`,
115
+ };
116
+ }
117
+ return { ok: true };
118
+ }
119
+ catch (err) {
120
+ return {
121
+ ok: false,
122
+ reason: err instanceof Error ? err.message : String(err),
123
+ };
124
+ }
125
+ }
79
126
  function resolveAttemptRoots(input, stageId) {
80
127
  const baseRoots = input.roots ??
81
128
  (input.workerRoots
@@ -136,15 +183,20 @@ export async function openStageAttempt(input) {
136
183
  const definitionId = input.stage.id;
137
184
  const completedEnvelopes = input.completedEnvelopes ??
138
185
  (await buildCompletedEnvelopesFromRun(input.store, input.runId, undefined, input.dag));
139
- const priorResult = await resolvePriorEnvelope({
140
- dag: input.dag,
141
- stageId,
142
- completedEnvelopes,
143
- store: input.store,
144
- runId: input.runId,
145
- });
186
+ const assigned = assignment(input.dag, stageId, definitionId, completedEnvelopes);
187
+ const priorResult = assigned ??
188
+ (await resolvePriorEnvelope({
189
+ dag: input.dag,
190
+ stageId,
191
+ completedEnvelopes,
192
+ store: input.store,
193
+ runId: input.runId,
194
+ }));
146
195
  if (!priorResult.ok)
147
196
  return priorResult;
197
+ const priorInput = assertPriorsMatchCloneInput(input.stage.clone_input_schema, stageId, priorResult, input.task, false);
198
+ if (!priorInput.ok)
199
+ return priorInput;
148
200
  const roots = resolveAttemptRoots(input, stageId);
149
201
  const attempt = input.attemptCtx?.attempt ?? 1;
150
202
  const resumeToken = input.resumeToken ??
@@ -152,9 +204,6 @@ export async function openStageAttempt(input) {
152
204
  const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
153
205
  const snapshot = input.dag;
154
206
  const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId, {
155
- ...(input.stage.clone_actions !== undefined
156
- ? { allowedActions: input.stage.clone_actions }
157
- : {}),
158
207
  ...(snapshot.clone_input_schema !== undefined
159
208
  ? { successorCloneInputSchemas: snapshot.clone_input_schema }
160
209
  : {}),
@@ -163,7 +212,7 @@ export async function openStageAttempt(input) {
163
212
  const runtimeNode = input.dag.nodes.find((node) => node.id === stageId);
164
213
  const isDynamicCloneInstance = runtimeNode?.definition_id !== undefined &&
165
214
  runtimeNode.definition_id !== runtimeNode.id;
166
- const feedbackLoopEmitContext = isDynamicCloneInstance || runtimeNode?.clonable === true
215
+ const feedbackLoopEmitContext = isDynamicCloneInstance
167
216
  ? undefined
168
217
  : runtimeNode?.feedback_loop;
169
218
  const repairContext = await repairContextForAttempt(input, stageId, attempt);
@@ -54,7 +54,7 @@ async function finalizeStageResult(options) {
54
54
  store,
55
55
  runId,
56
56
  stageId,
57
- event: { event: "failed", reason: result.reason },
57
+ event: { event: "failed", reason: result.reason, usage: result.usage },
58
58
  attemptCtx,
59
59
  });
60
60
  console.error(`Stage ${stageId} failed: ${result.reason}`);
@@ -64,7 +64,7 @@ async function finalizeStageResult(options) {
64
64
  store,
65
65
  runId,
66
66
  stageId,
67
- event: { event: "succeeded" },
67
+ event: { event: "succeeded", usage: result.usage },
68
68
  attemptCtx,
69
69
  });
70
70
  console.error(`Stage ${stageId} succeeded.`);
@@ -34,21 +34,9 @@ export function checkTaskEntryInput(task, loaded, options) {
34
34
  const stage = stageById.get(rootId);
35
35
  if (stage?.clone_input_schema === undefined)
36
36
  continue;
37
- if (task.input === undefined) {
38
- findings.push({
39
- severity: "warning",
40
- code: "task.entry_input_unmet",
41
- path: findingPath,
42
- message: `Task has no input; entry stage "${rootId}" requires io.input`,
43
- category: "task",
44
- pipelineId: loaded.pipeline.id,
45
- stageId: rootId,
46
- });
47
- continue;
48
- }
49
37
  let details;
50
38
  try {
51
- details = payloadInstanceMismatch(task.input, stage.clone_input_schema);
39
+ details = payloadInstanceMismatch(task.input ?? {}, stage.clone_input_schema);
52
40
  }
53
41
  catch (err) {
54
42
  const message = err instanceof Error ? err.message : String(err);
@@ -14,12 +14,12 @@ import type { TSchema } from "typebox/type";
14
14
  import { type PreEmitCheckOptions } from "../envelope/preEmitChecks.js";
15
15
  import type { StageEnvelope } from "../types/envelope.js";
16
16
  import type { FeedbackLoopConfig } from "../types/pipeline.js";
17
- import { type CloneEmitContext, type ForkEmitContext } from "../types/forkChoice.js";
17
+ import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
18
18
  export type EmitCapture = {
19
19
  envelope?: StageEnvelope;
20
20
  error?: string;
21
21
  };
22
- export declare function createEmitStageEnvelopeTool(capture: EmitCapture, payloadSchema?: unknown, forkEmitContext?: ForkEmitContext, cloneEmitContext?: CloneEmitContext, preEmitCheckOptions?: PreEmitCheckOptions, feedbackLoopEmitContext?: FeedbackLoopConfig): {
22
+ export declare function createEmitStageEnvelopeTool(capture: EmitCapture, payloadSchema?: unknown, forkEmitContext?: ForkEmitContext, _cloneEmitContext?: CloneEmitContext, preEmitCheckOptions?: PreEmitCheckOptions, feedbackLoopEmitContext?: FeedbackLoopConfig): {
23
23
  name: string;
24
24
  label: string;
25
25
  description: string;
@@ -29,7 +29,6 @@ export declare function createEmitStageEnvelopeTool(capture: EmitCapture, payloa
29
29
  artifacts: Type.TArray<Type.TString>;
30
30
  payload: Type.TOptional<TSchema>;
31
31
  fork_choice: Type.TArray<Type.TString>;
32
- clone_forks?: Type.TArray<TSchema> | undefined;
33
32
  feedback_loop?: Type.TOptional<TSchema> | undefined;
34
33
  checklist_attestations: Type.TOptional<Type.TArray<Type.TObject<{
35
34
  check_id: Type.TString;
@@ -12,11 +12,9 @@
12
12
  import { Type } from "typebox";
13
13
  import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
14
14
  import { assertFeedbackLoopAction } from "../envelope/feedbackLoop.js";
15
- import { assertCloneForks } from "../envelope/cloneForks.js";
16
15
  import { normalizeForkChoice } from "../envelope/forkChoice.js";
17
16
  import { assertEnvelopePayload, compilePayloadSchema, } from "../envelope/payloadSchema.js";
18
17
  import { assertPreEmitChecks, } from "../envelope/preEmitChecks.js";
19
- import { CLONE_ACTIONS, } from "../types/forkChoice.js";
20
18
  function stringLiteralsSchema(values) {
21
19
  if (values.length === 1) {
22
20
  return Type.Literal(values[0]);
@@ -42,56 +40,6 @@ function feedbackLoopActionSchema(context) {
42
40
  }, { additionalProperties: false }),
43
41
  ]);
44
42
  }
45
- function cloneForksItemSchema(cloneEmitContext) {
46
- const allowedActions = cloneEmitContext.allowedActions ?? [...CLONE_ACTIONS];
47
- const allowSkip = allowedActions.includes("skip");
48
- const allowOnce = allowedActions.includes("once");
49
- const allowFanout = allowedActions.includes("fanout");
50
- const variants = [];
51
- for (const successor of cloneEmitContext.clonableSuccessors) {
52
- const successorId = Type.Literal(successor.successorId);
53
- const nestedPayload = successor.cloneInputSchema !== undefined
54
- ? compilePayloadSchema(successor.cloneInputSchema)
55
- : undefined;
56
- const nestedEnvelope = stageEnvelopeSchema(nestedPayload);
57
- if (allowSkip) {
58
- variants.push(Type.Object({
59
- successor_id: successorId,
60
- action: Type.Literal("skip"),
61
- }, { additionalProperties: false }));
62
- }
63
- if (allowOnce) {
64
- variants.push(Type.Object({
65
- successor_id: successorId,
66
- action: Type.Literal("once"),
67
- envelope: nestedEnvelope,
68
- }));
69
- }
70
- if (allowFanout) {
71
- variants.push(Type.Object({
72
- successor_id: successorId,
73
- action: Type.Literal("fanout"),
74
- mode: Type.Union([
75
- Type.Literal("parallel"),
76
- Type.Literal("sequential"),
77
- ]),
78
- clones: Type.Array(Type.Object({
79
- envelope: nestedEnvelope,
80
- }), { minItems: 2, maxItems: successor.cloneCap }),
81
- }));
82
- }
83
- }
84
- if (variants.length === 0) {
85
- return Type.Object({
86
- successor_id: Type.String(),
87
- action: stringLiteralsSchema(allowedActions),
88
- });
89
- }
90
- if (variants.length === 1) {
91
- return variants[0];
92
- }
93
- return Type.Union(variants);
94
- }
95
43
  function toolResult(text, details, options) {
96
44
  return {
97
45
  content: [{ type: "text", text }],
@@ -100,7 +48,7 @@ function toolResult(text, details, options) {
100
48
  ...(options?.terminate ? { terminate: true } : {}),
101
49
  };
102
50
  }
103
- export function createEmitStageEnvelopeTool(capture, payloadSchema, forkEmitContext, cloneEmitContext, preEmitCheckOptions, feedbackLoopEmitContext) {
51
+ export function createEmitStageEnvelopeTool(capture, payloadSchema, forkEmitContext, _cloneEmitContext, preEmitCheckOptions, feedbackLoopEmitContext) {
104
52
  const compiledPayload = payloadSchema !== undefined
105
53
  ? compilePayloadSchema(payloadSchema)
106
54
  : undefined;
@@ -118,13 +66,6 @@ export function createEmitStageEnvelopeTool(capture, payloadSchema, forkEmitCont
118
66
  fork_choice: forkEmitContext !== undefined && feedbackLoopEmitContext === undefined
119
67
  ? Type.Array(Type.String())
120
68
  : Type.Optional(Type.Array(Type.String())),
121
- ...(cloneEmitContext !== undefined
122
- ? {
123
- clone_forks: feedbackLoopEmitContext === undefined
124
- ? Type.Array(cloneForksItemSchema(cloneEmitContext))
125
- : Type.Optional(Type.Array(cloneForksItemSchema(cloneEmitContext))),
126
- }
127
- : {}),
128
69
  ...(feedbackLoopEmitContext !== undefined
129
70
  ? {
130
71
  feedback_loop: Type.Optional(feedbackLoopActionSchema(feedbackLoopEmitContext)),
@@ -150,11 +91,6 @@ export function createEmitStageEnvelopeTool(capture, payloadSchema, forkEmitCont
150
91
  forkEmitContext !== undefined) {
151
92
  normalizeForkChoice(envelope.fork_choice, "emit", forkEmitContext);
152
93
  }
153
- if (envelope.status !== "failure" &&
154
- !isSendBack &&
155
- cloneEmitContext !== undefined) {
156
- assertCloneForks(envelope, cloneEmitContext);
157
- }
158
94
  assertEnvelopePayload(envelope, payloadSchema);
159
95
  if (envelope.status !== "failure") {
160
96
  await assertPreEmitChecks(envelope, preEmitCheckOptions);
@@ -1,4 +1,3 @@
1
- import type { CloneForkItem } from "./forkChoice.js";
2
1
  export type EnvelopeStatus = "success" | "failure";
3
2
  export type FeedbackLoopAction = {
4
3
  action: "continue";
@@ -12,7 +11,6 @@ export type StageEnvelope = {
12
11
  artifacts: string[];
13
12
  payload?: Record<string, unknown>;
14
13
  fork_choice?: string[];
15
- clone_forks?: CloneForkItem[];
16
14
  feedback_loop?: FeedbackLoopAction;
17
15
  /** Agent's explicit acknowledgement of pipeline-owned checklist items. */
18
16
  checklist_attestations?: Array<{
@@ -1,4 +1,3 @@
1
- import type { StageEnvelope } from "./envelope.js";
2
1
  export type ForkChoiceShape = {
3
2
  cardinality: "one" | "subset";
4
3
  allowNone: boolean;
@@ -9,21 +8,6 @@ export type ForkEmitContext = {
9
8
  };
10
9
  export declare const CLONE_ACTIONS: readonly ["skip", "once", "fanout"];
11
10
  export type CloneAction = (typeof CLONE_ACTIONS)[number];
12
- export type CloneForkItem = {
13
- successor_id: string;
14
- action: "skip";
15
- } | {
16
- successor_id: string;
17
- action: "once";
18
- envelope: StageEnvelope;
19
- } | {
20
- successor_id: string;
21
- action: "fanout";
22
- mode: "parallel" | "sequential";
23
- clones: Array<{
24
- envelope: StageEnvelope;
25
- }>;
26
- };
27
11
  export type CloneSuccessorEmit = {
28
12
  successorId: string;
29
13
  cloneCap: number;
@@ -1,4 +1,3 @@
1
- import type { CloneAction } from "./forkChoice.js";
2
1
  import type { LoadedStageConfig, StageGateKind, StageIoYaml } from "./stage.js";
3
2
  import type { CompletionContract, RecoveryPolicy } from "./completion.js";
4
3
  export type PipelineConfig = {
@@ -16,15 +15,75 @@ export type PipelineForkConfig = {
16
15
  allow_none: boolean;
17
16
  };
18
17
  export type NeedTerminalState = "succeeded" | "failed" | "skipped";
18
+ export type RouteIfOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "not_in";
19
+ export type RouteIfLeafPredicate = {
20
+ field: string;
21
+ op: RouteIfOp;
22
+ value: unknown;
23
+ };
24
+ export type RouteIfAllPredicate = {
25
+ all: RouteIfPredicate[];
26
+ field?: never;
27
+ };
28
+ export type RouteIfAnyPredicate = {
29
+ any: RouteIfPredicate[];
30
+ field?: never;
31
+ };
32
+ export type RouteIfNotPredicate = {
33
+ not: RouteIfPredicate;
34
+ field?: never;
35
+ };
36
+ export type RouteIfPredicate = RouteIfLeafPredicate | RouteIfAllPredicate | RouteIfAnyPredicate | RouteIfNotPredicate;
19
37
  export type PipelineNeedEdge = {
20
38
  id: string;
21
39
  on: NeedTerminalState[];
40
+ if?: RouteIfPredicate;
22
41
  };
23
42
  export type PipelineNeedItem = string | {
24
43
  id: string;
25
44
  on?: NeedTerminalState[];
26
45
  };
27
46
  export type PipelineNeeds = string | PipelineNeedEdge[];
47
+ /**
48
+ * Terminal states a route entry (or its inverted predecessor edge) can gate on.
49
+ * Same domain as `NeedTerminalState` — kept as a distinct alias since `route`
50
+ * is a separate vocabulary from `needs` (see docs/yaml-catalog.md).
51
+ */
52
+ export type RouteTerminalState = NeedTerminalState;
53
+ /**
54
+ * A forward route entry: hands off from the declaring stage to `to`, gated on
55
+ * the declaring stage's own terminal state (`on`, defaulting to succeeded-only).
56
+ */
57
+ export type PipelineRouteForwardEntry = {
58
+ to: string;
59
+ on?: RouteTerminalState | RouteTerminalState[];
60
+ if?: RouteIfPredicate;
61
+ };
62
+ /**
63
+ * A loop route entry (ticket 03, route-based-pipeline-wiring spec): sends
64
+ * execution back to a declared ancestor of the source stage instead of
65
+ * continuing forward, carrying the same replay policy `FeedbackLoopConfig`
66
+ * carries today — `target` renamed to `to` for naming consistency with
67
+ * forward entries. Contributes no forward DAG edge (`toRouteEdges` skips
68
+ * it), so it is excluded from cycle detection; it is a runtime-only replay
69
+ * schedule over the already-resolved forward graph.
70
+ */
71
+ export type PipelineRouteLoopEntry = {
72
+ type: "loop";
73
+ to: string;
74
+ max_replays: number;
75
+ on_max_replays: "require_continue" | "wait_for_human";
76
+ replay_session: "resume" | "new_session";
77
+ };
78
+ export type PipelineRouteEntry = PipelineRouteForwardEntry | PipelineRouteLoopEntry;
79
+ /** A stage's outbound `route` field: where execution goes next. */
80
+ export type PipelineRoute = PipelineRouteEntry[];
81
+ /** Normalized forward route edge (post-parse): `on` is always populated. */
82
+ export type PipelineRouteEdge = {
83
+ to: string;
84
+ on: RouteTerminalState[];
85
+ if?: RouteIfPredicate;
86
+ };
28
87
  /** Runtime feedback-loop policy declared by the stage that can send work back. */
29
88
  export type FeedbackLoopConfig = {
30
89
  target: string;
@@ -32,43 +91,44 @@ export type FeedbackLoopConfig = {
32
91
  on_max_replays: "require_continue" | "wait_for_human";
33
92
  replay_session: "resume" | "new_session";
34
93
  };
94
+ export type CloneMode = "parallel" | "sequential";
35
95
  export type PipelineStageRef = {
36
96
  id: string;
37
- needs?: string | PipelineNeedItem[];
38
- fork?: {
39
- select: "one" | "subset";
40
- allow_none?: boolean;
41
- };
42
- clonable?: boolean;
43
- clone_cap?: number;
44
97
  /** IR: after-phase checks. YAML: `verify` items whose `when` includes `after`. */
45
98
  completion?: CompletionContract;
46
99
  /** IR: after-phase failure policy. YAML: `on_verify_fail`. */
47
100
  recovery?: RecoveryPolicy;
48
- feedback_loop?: FeedbackLoopConfig;
49
101
  /** Omitted means this stage is safe to include in a feedback replay. */
50
102
  replay_safe?: boolean;
103
+ /** Pipeline wiring vocabulary (see docs/yaml-catalog.md). */
104
+ route?: PipelineRouteEntry[];
105
+ /** Marks this stage as a pipeline entry point. */
106
+ entry?: boolean;
107
+ clone_cap?: number;
108
+ clone_mode?: CloneMode;
51
109
  };
52
110
  /**
53
- * Pipeline YAML stage entry (both dialects). Target authoring is `io` / `verify`
54
- * / `on_verify_fail`. The payload_schema / pre_emit_checks / completion /
55
- * recovery keys are the legacy YAML dialect (and also the IR names after compile).
56
- * New catalog fields: add target YAML keys here and compile them in yamlDialect.ts.
111
+ * Catalog YAML stage entry (`io` / `verify` / `on_verify_fail`). Not IR.
112
+ * Dual-read of `payload_schema` / `pre_emit_checks` / `completion` / `recovery`
113
+ * stays in `legacyYaml.ts` on the raw record.
57
114
  */
58
- export type PipelineStageYamlEntry = PipelineStageRef & {
115
+ export type PipelineStageYamlEntry = {
116
+ id?: string;
59
117
  uses?: string;
60
118
  system_prompt?: string;
61
119
  model?: string;
62
- payload_schema?: unknown;
63
120
  gate_kinds?: StageGateKind[];
64
- clone_input_schema?: unknown;
65
- clone_actions?: CloneAction[];
121
+ timeout_ms?: number;
66
122
  skill?: string;
67
123
  mcp?: string[];
124
+ replay_safe?: boolean;
125
+ route?: PipelineRouteEntry[];
126
+ entry?: boolean;
127
+ clone_cap?: number;
128
+ clone_mode?: CloneMode;
68
129
  io?: StageIoYaml;
69
130
  verify?: unknown;
70
131
  on_verify_fail?: RecoveryPolicy;
71
- pre_emit_checks?: unknown;
72
132
  };
73
133
  export type PipelineIncludeEntry = {
74
134
  local: string;
@@ -79,20 +139,16 @@ export type PipelineFragmentConfig = {
79
139
  };
80
140
  export type NormalizedPipelineStageEntry = {
81
141
  id: string;
82
- needs?: PipelineNeeds;
83
- fork?: {
84
- select: "one" | "subset";
85
- allow_none?: boolean;
86
- };
87
- clonable?: boolean;
88
- clone_cap?: number;
89
142
  /** IR after compile. YAML `verify` after-phase / `on_verify_fail`. */
90
143
  completion?: CompletionContract;
91
144
  recovery?: RecoveryPolicy;
92
- feedback_loop?: FeedbackLoopConfig;
93
145
  replay_safe?: boolean;
146
+ route?: PipelineRouteEntry[];
147
+ entry?: boolean;
94
148
  skill?: string;
95
149
  mcp?: string[];
150
+ clone_cap?: number;
151
+ clone_mode?: CloneMode;
96
152
  body: {
97
153
  kind: "inline";
98
154
  raw: Record<string, unknown>;
@@ -116,8 +172,9 @@ export type ResolvedPipelineStageNode = {
116
172
  ancestors: string[];
117
173
  stageIndex: number;
118
174
  fork?: PipelineForkConfig;
119
- clonable?: boolean;
120
175
  clone_cap?: number;
176
+ clone_mode?: CloneMode;
177
+ clone_array_field?: string;
121
178
  definition_id?: string;
122
179
  /** Persisted on pipeline_dag. YAML `verify` after-phase. */
123
180
  completion?: CompletionContract;
@@ -125,6 +182,8 @@ export type ResolvedPipelineStageNode = {
125
182
  recovery?: RecoveryPolicy;
126
183
  feedback_loop?: FeedbackLoopConfig;
127
184
  replay_safe?: boolean;
185
+ /** Present (true) only when this stage was declared `entry: true`. */
186
+ entry?: boolean;
128
187
  };
129
188
  export type ResolvedPipelineDag = {
130
189
  nodes: ResolvedPipelineStageNode[];
@@ -1,14 +1,19 @@
1
1
  import type { PreEmitCheck } from "./preEmitCheck.js";
2
- import type { CloneAction } from "./forkChoice.js";
2
+ /** Typed emit/schema fields from `compileTargetContract`. Not catalog YAML keys. */
3
+ export type CompiledStageEmitBody = {
4
+ payload_schema?: unknown;
5
+ clone_input_schema?: unknown;
6
+ pre_emit_checks?: PreEmitCheck[];
7
+ };
3
8
  export declare const STAGE_GATE_KINDS: readonly ["free_text", "confirm", "multi_question", "artifact_backed"];
4
9
  export type StageGateKind = (typeof STAGE_GATE_KINDS)[number];
5
10
  /** Target YAML `io:` block. Compiles onto StageConfig.payload_schema / clone_input_schema. */
6
11
  export type StageIoYaml = {
7
- input?: {
8
- schema?: unknown;
12
+ input: {
13
+ schema: unknown;
9
14
  };
10
- output?: {
11
- schema?: unknown;
15
+ output: {
16
+ schema: unknown;
12
17
  };
13
18
  };
14
19
  /**
@@ -27,9 +32,8 @@ export type StageConfig = {
27
32
  gate_kinds?: StageGateKind[];
28
33
  /** IR: emit-phase checks. YAML: `verify` items whose `when` includes `emit`. */
29
34
  pre_emit_checks?: PreEmitCheck[];
30
- /** IR: clone/fan-out assignment schema. YAML: `io.input.schema`. */
35
+ /** IR: inbound assignment schema. YAML: `io.input.schema`. */
31
36
  clone_input_schema?: unknown;
32
- clone_actions?: CloneAction[];
33
37
  /** Optional stage wall-clock timeout in milliseconds (default 60 minutes). */
34
38
  timeout_ms?: number;
35
39
  skill?: string;
@@ -0,0 +1,16 @@
1
+ /** Per-model token/cost breakdown, as computed by the backend SDK itself. */
2
+ export type ModelUsageBreakdown = {
3
+ inputTokens: number;
4
+ outputTokens: number;
5
+ cacheReadInputTokens: number;
6
+ cacheCreationInputTokens: number;
7
+ costUsd: number;
8
+ };
9
+ /** Accumulated LLM usage/cost for one stage attempt, across every turn/call it made. */
10
+ export type StageUsage = {
11
+ costUsd: number;
12
+ models: Record<string, ModelUsageBreakdown>;
13
+ };
14
+ /** Merge one model's usage from a single call into an accumulator, in place. */
15
+ export declare function addModelUsage(usage: StageUsage, model: string, delta: Partial<ModelUsageBreakdown>): void;
16
+ export declare function emptyStageUsage(): StageUsage;
@@ -0,0 +1,24 @@
1
+ function emptyBreakdown() {
2
+ return {
3
+ inputTokens: 0,
4
+ outputTokens: 0,
5
+ cacheReadInputTokens: 0,
6
+ cacheCreationInputTokens: 0,
7
+ costUsd: 0,
8
+ };
9
+ }
10
+ /** Merge one model's usage from a single call into an accumulator, in place. */
11
+ export function addModelUsage(usage, model, delta) {
12
+ const current = usage.models[model] ?? emptyBreakdown();
13
+ usage.models[model] = {
14
+ inputTokens: current.inputTokens + (delta.inputTokens ?? 0),
15
+ outputTokens: current.outputTokens + (delta.outputTokens ?? 0),
16
+ cacheReadInputTokens: current.cacheReadInputTokens + (delta.cacheReadInputTokens ?? 0),
17
+ cacheCreationInputTokens: current.cacheCreationInputTokens + (delta.cacheCreationInputTokens ?? 0),
18
+ costUsd: current.costUsd + (delta.costUsd ?? 0),
19
+ };
20
+ usage.costUsd += delta.costUsd ?? 0;
21
+ }
22
+ export function emptyStageUsage() {
23
+ return { costUsd: 0, models: {} };
24
+ }