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
@@ -0,0 +1,215 @@
1
+ import { isNeedTerminalState } from "./pipelineNeeds.js";
2
+ import { parseRouteIf } from "./routeIf.js";
3
+ function isPlainObject(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function isForwardRouteEntry(entry) {
7
+ return !("type" in entry) || entry.type === undefined;
8
+ }
9
+ export function isLoopRouteEntry(entry) {
10
+ return "type" in entry && entry.type === "loop";
11
+ }
12
+ const ALLOWED_LOOP_ITEM_KEYS = new Set([
13
+ "type",
14
+ "to",
15
+ "max_replays",
16
+ "on_max_replays",
17
+ "replay_session",
18
+ ]);
19
+ function parseRouteLoopEntry(item, stageId) {
20
+ if ("if" in item) {
21
+ return {
22
+ ok: false,
23
+ message: `stage "${stageId}": if is not allowed on a loop route entry`,
24
+ code: "pipeline.route_if_invalid",
25
+ };
26
+ }
27
+ for (const key of Object.keys(item)) {
28
+ if (!ALLOWED_LOOP_ITEM_KEYS.has(key)) {
29
+ return {
30
+ ok: false,
31
+ message: `stage "${stageId}": route item: unknown key "${key}"`,
32
+ };
33
+ }
34
+ }
35
+ if (typeof item.to !== "string" || !item.to) {
36
+ return {
37
+ ok: false,
38
+ message: `stage "${stageId}": route item must have a non-empty "to" stage id`,
39
+ };
40
+ }
41
+ if (!Number.isInteger(item.max_replays) || item.max_replays < 1) {
42
+ return {
43
+ ok: false,
44
+ message: `stage "${stageId}": route "${item.to}": max_replays must be a positive integer`,
45
+ };
46
+ }
47
+ if (item.on_max_replays !== "require_continue" && item.on_max_replays !== "wait_for_human") {
48
+ return {
49
+ ok: false,
50
+ message: `stage "${stageId}": route "${item.to}": on_max_replays must be "require_continue" or "wait_for_human"`,
51
+ };
52
+ }
53
+ if (item.replay_session !== "resume" && item.replay_session !== "new_session") {
54
+ return {
55
+ ok: false,
56
+ message: `stage "${stageId}": route "${item.to}": replay_session must be "resume" or "new_session"`,
57
+ };
58
+ }
59
+ return {
60
+ ok: true,
61
+ value: {
62
+ type: "loop",
63
+ to: item.to,
64
+ max_replays: item.max_replays,
65
+ on_max_replays: item.on_max_replays,
66
+ replay_session: item.replay_session,
67
+ },
68
+ };
69
+ }
70
+ function parseRouteOn(raw, stageId, targetId) {
71
+ if (raw === undefined) {
72
+ return { ok: true, value: ["succeeded"] };
73
+ }
74
+ const items = Array.isArray(raw) ? raw : [raw];
75
+ if (items.length === 0) {
76
+ return {
77
+ ok: false,
78
+ message: `stage "${stageId}": route "${targetId}": on must be a non-empty unique subset of succeeded, failed, skipped`,
79
+ };
80
+ }
81
+ const seen = new Set();
82
+ const on = [];
83
+ for (const item of items) {
84
+ if (typeof item !== "string" || !isNeedTerminalState(item) || seen.has(item)) {
85
+ return {
86
+ ok: false,
87
+ message: `stage "${stageId}": route "${targetId}": on must be a non-empty unique subset of succeeded, failed, skipped`,
88
+ };
89
+ }
90
+ seen.add(item);
91
+ on.push(item);
92
+ }
93
+ return { ok: true, value: on };
94
+ }
95
+ export function parsePipelineRoute(raw, stageId) {
96
+ if (!Array.isArray(raw)) {
97
+ return {
98
+ ok: false,
99
+ message: `stage "${stageId}": route must be a non-empty array`,
100
+ };
101
+ }
102
+ if (raw.length < 1) {
103
+ return {
104
+ ok: false,
105
+ message: `stage "${stageId}": route array must contain at least one item`,
106
+ };
107
+ }
108
+ const entries = [];
109
+ const seenTargets = new Set();
110
+ for (const item of raw) {
111
+ if (!isPlainObject(item)) {
112
+ return {
113
+ ok: false,
114
+ message: `stage "${stageId}": route item must be an object { to, on? }`,
115
+ };
116
+ }
117
+ if (item.type !== undefined && item.type !== "loop") {
118
+ return {
119
+ ok: false,
120
+ message: `stage "${stageId}": route item: type must be "loop" if present, got "${String(item.type)}"`,
121
+ };
122
+ }
123
+ if (item.type === "loop") {
124
+ const loopResult = parseRouteLoopEntry(item, stageId);
125
+ if (!loopResult.ok)
126
+ return loopResult;
127
+ if (seenTargets.has(loopResult.value.to)) {
128
+ return {
129
+ ok: false,
130
+ message: `stage "${stageId}": route contains duplicate target "${loopResult.value.to}"`,
131
+ };
132
+ }
133
+ seenTargets.add(loopResult.value.to);
134
+ entries.push(loopResult.value);
135
+ continue;
136
+ }
137
+ for (const key of Object.keys(item)) {
138
+ if (key !== "to" && key !== "on" && key !== "if") {
139
+ return {
140
+ ok: false,
141
+ message: `stage "${stageId}": route item: unknown key "${key}"`,
142
+ };
143
+ }
144
+ }
145
+ if (typeof item.to !== "string" || !item.to) {
146
+ return {
147
+ ok: false,
148
+ message: `stage "${stageId}": route item must have a non-empty "to" stage id`,
149
+ };
150
+ }
151
+ if (seenTargets.has(item.to)) {
152
+ return {
153
+ ok: false,
154
+ message: `stage "${stageId}": route contains duplicate target "${item.to}"`,
155
+ };
156
+ }
157
+ const onResult = parseRouteOn(item.on, stageId, item.to);
158
+ if (!onResult.ok)
159
+ return onResult;
160
+ let routeIf;
161
+ if (item.if !== undefined) {
162
+ const ifResult = parseRouteIf(item.if, stageId, item.to);
163
+ if (!ifResult.ok)
164
+ return ifResult;
165
+ if (onResult.value.length !== 1 || onResult.value[0] !== "succeeded") {
166
+ return {
167
+ ok: false,
168
+ message: `stage "${stageId}": route "${item.to}": if is only allowed with on succeeded`,
169
+ code: "pipeline.route_if_invalid",
170
+ };
171
+ }
172
+ routeIf = ifResult.value;
173
+ }
174
+ seenTargets.add(item.to);
175
+ entries.push({
176
+ to: item.to,
177
+ on: onResult.value,
178
+ ...(routeIf !== undefined ? { if: routeIf } : {}),
179
+ });
180
+ }
181
+ return { ok: true, value: entries };
182
+ }
183
+ /**
184
+ * Normalize an already-typed `route` value (as found on a `PipelineStageRef`,
185
+ * which may be hand-built in TypeScript and not have gone through
186
+ * `parsePipelineRoute`) into forward route edges with `on` always populated.
187
+ * Loop entries are skipped — they contribute no forward edge.
188
+ */
189
+ export function toRouteEdges(route) {
190
+ if (route === undefined)
191
+ return [];
192
+ const edges = [];
193
+ for (const item of route) {
194
+ if (!isForwardRouteEntry(item))
195
+ continue;
196
+ const on = item.on === undefined ? ["succeeded"] : Array.isArray(item.on) ? item.on : [item.on];
197
+ edges.push({
198
+ to: item.to,
199
+ on: on,
200
+ ...(item.if !== undefined ? { if: item.if } : {}),
201
+ });
202
+ }
203
+ return edges;
204
+ }
205
+ /**
206
+ * Extract the loop entries from an already-typed `route` value (mirrors
207
+ * `toRouteEdges`, the forward-entry counterpart). Used by the resolver to
208
+ * validate replay policy and synthesize the legacy `feedback_loop` shape on
209
+ * the resolved DAG node.
210
+ */
211
+ export function toRouteLoopEntries(route) {
212
+ if (route === undefined)
213
+ return [];
214
+ return route.filter(isLoopRouteEntry);
215
+ }
@@ -11,5 +11,7 @@
11
11
  export declare const WIRING_KEYS: Set<string>;
12
12
  /** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
13
13
  export declare const BODY_KEYS: Set<string>;
14
+ /** Recognized only so catalog load can name the field and point at Clone Chain. */
15
+ export declare const REJECTED_CLONE_PRODUCT_KEYS: Set<string>;
14
16
  export declare function isPipelineStageBodyKey(key: string): boolean;
15
17
  export declare function isAllowedPipelineStageEntryKey(key: string): boolean;
@@ -13,13 +13,18 @@ export const WIRING_KEYS = new Set([
13
13
  "needs",
14
14
  "fork",
15
15
  "uses",
16
- "clonable",
17
- "clone_cap",
18
16
  "completion",
19
17
  "recovery",
20
18
  "on_verify_fail",
21
19
  "feedback_loop",
22
20
  "replay_safe",
21
+ /** Additive, alongside `needs`/`fork`/`feedback_loop` (see docs/yaml-catalog.md). */
22
+ "route",
23
+ "entry",
24
+ "route_select",
25
+ "allow_none",
26
+ "clone_cap",
27
+ "clone_mode",
23
28
  ]);
24
29
  /** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
25
30
  export const BODY_KEYS = new Set([
@@ -29,16 +34,22 @@ export const BODY_KEYS = new Set([
29
34
  "gate_kinds",
30
35
  "pre_emit_checks",
31
36
  "clone_input_schema",
32
- "clone_actions",
33
37
  "timeout_ms",
34
38
  "skill",
35
39
  "mcp",
36
40
  "io",
37
41
  "verify",
38
42
  ]);
43
+ /** Recognized only so catalog load can name the field and point at Clone Chain. */
44
+ export const REJECTED_CLONE_PRODUCT_KEYS = new Set([
45
+ "clonable",
46
+ "clone_actions",
47
+ ]);
39
48
  export function isPipelineStageBodyKey(key) {
40
49
  return BODY_KEYS.has(key);
41
50
  }
42
51
  export function isAllowedPipelineStageEntryKey(key) {
43
- return WIRING_KEYS.has(key) || BODY_KEYS.has(key);
52
+ return (WIRING_KEYS.has(key) ||
53
+ BODY_KEYS.has(key) ||
54
+ REJECTED_CLONE_PRODUCT_KEYS.has(key));
44
55
  }
@@ -112,19 +112,28 @@ export function buildVerifyItems(preEmit, afterChecks) {
112
112
  }
113
113
  return { ok: true, items: order.map((id) => byId.get(id)) };
114
114
  }
115
+ const OBJECT_SCHEMA = { type: "object" };
115
116
  export function ioFromSchemas(stage) {
116
- const io = {};
117
- if (stage.clone_input_schema !== undefined) {
118
- io.input = { schema: stage.clone_input_schema };
119
- }
120
- if (stage.payload_schema !== undefined) {
121
- io.output = { schema: stage.payload_schema };
117
+ if (stage.clone_input_schema === undefined && stage.payload_schema === undefined) {
118
+ return undefined;
122
119
  }
123
- return io.input !== undefined || io.output !== undefined ? io : undefined;
120
+ return {
121
+ input: { schema: stage.clone_input_schema ?? OBJECT_SCHEMA },
122
+ output: { schema: stage.payload_schema ?? OBJECT_SCHEMA },
123
+ };
124
124
  }
125
125
  export function ioFromRawDocument(raw) {
126
- if (isPlainObject(raw.io))
127
- return raw.io;
126
+ if (isPlainObject(raw.io)) {
127
+ const io = raw.io;
128
+ const input = isPlainObject(io.input) ? io.input : undefined;
129
+ const output = isPlainObject(io.output) ? io.output : undefined;
130
+ if (input?.schema === undefined && output?.schema === undefined)
131
+ return undefined;
132
+ return {
133
+ input: { schema: input?.schema ?? OBJECT_SCHEMA },
134
+ output: { schema: output?.schema ?? OBJECT_SCHEMA },
135
+ };
136
+ }
128
137
  return ioFromSchemas({
129
138
  payload_schema: raw.payload_schema,
130
139
  clone_input_schema: raw.clone_input_schema,
@@ -148,7 +157,6 @@ const STAGE_BODY_ORDER = [
148
157
  "io",
149
158
  "verify",
150
159
  "gate_kinds",
151
- "clone_actions",
152
160
  "skill",
153
161
  "mcp",
154
162
  ];
@@ -159,8 +167,6 @@ const PIPELINE_WIRING_ORDER = [
159
167
  "mcp",
160
168
  "needs",
161
169
  "fork",
162
- "clonable",
163
- "clone_cap",
164
170
  "on_verify_fail",
165
171
  "feedback_loop",
166
172
  "replay_safe",
@@ -4,5 +4,5 @@ export type ResolveCloneEmitOptions = {
4
4
  allowedActions?: CloneAction[];
5
5
  successorCloneInputSchemas?: Record<string, unknown>;
6
6
  };
7
- export declare function resolveCloneEmitContext(dag: ResolvedPipelineDag, stageId: string, options?: ResolveCloneEmitOptions): CloneEmitContext | undefined;
7
+ export declare function resolveCloneEmitContext(_dag: ResolvedPipelineDag, _stageId: string, _options?: ResolveCloneEmitOptions): CloneEmitContext | undefined;
8
8
  export declare function resolveForkEmitContext(dag: ResolvedPipelineDag, stageId: string): ForkEmitContext | undefined;
@@ -1,34 +1,11 @@
1
- export function resolveCloneEmitContext(dag, stageId, options) {
2
- const childIds = dag.childrenOf[stageId] ?? [];
3
- const clonableSuccessors = [];
4
- for (const childId of childIds) {
5
- const node = dag.nodes.find((n) => n.id === childId);
6
- if (node?.clonable === true) {
7
- const cloneInputSchema = options?.successorCloneInputSchemas?.[childId];
8
- clonableSuccessors.push({
9
- successorId: childId,
10
- cloneCap: node.clone_cap,
11
- ...(cloneInputSchema !== undefined ? { cloneInputSchema } : {}),
12
- });
13
- }
14
- }
15
- if (clonableSuccessors.length === 0)
16
- return undefined;
17
- return {
18
- clonableSuccessors,
19
- ...(options?.allowedActions !== undefined
20
- ? { allowedActions: options.allowedActions }
21
- : {}),
22
- };
1
+ export function resolveCloneEmitContext(_dag, _stageId, _options) {
2
+ return undefined;
23
3
  }
24
4
  export function resolveForkEmitContext(dag, stageId) {
25
5
  const dagNode = dag.nodes.find((n) => n.id === stageId);
26
6
  if (!dagNode?.fork)
27
7
  return undefined;
28
- const immediateSuccessorIds = (dag.childrenOf[stageId] ?? []).filter((childId) => {
29
- const child = dag.nodes.find((n) => n.id === childId);
30
- return child?.clonable !== true;
31
- });
8
+ const immediateSuccessorIds = dag.childrenOf[stageId] ?? [];
32
9
  if (immediateSuccessorIds.length === 0)
33
10
  return undefined;
34
11
  return {
@@ -1,10 +1,8 @@
1
- import type { PipelineStageRef, PipelineStageYamlEntry, ResolvedPipelineDag, FeedbackLoopConfig } from "../types/pipeline.js";
1
+ import type { PipelineStageRef, ResolvedPipelineDag } from "../types/pipeline.js";
2
2
  export type ResolvePipelineDagContext = {
3
3
  pipelineId: string;
4
4
  path: string;
5
5
  };
6
- export declare function parseFeedbackLoopConfig(value: unknown, stageId: string, ctx: ResolvePipelineDagContext): FeedbackLoopConfig;
7
- export declare function parsePipelineStageEntries(raw: unknown, ctx: ResolvePipelineDagContext): PipelineStageYamlEntry[];
8
6
  export declare function resolvePipelineDagFromRefs(refs: PipelineStageRef[], ctx: ResolvePipelineDagContext): {
9
7
  stages: string[];
10
8
  dag: ResolvedPipelineDag;