stageflow 0.16.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 (96) hide show
  1. package/README.md +2 -4
  2. package/dist/agent/claudeTools.d.ts +1 -4
  3. package/dist/agent/claudeTools.js +1 -6
  4. package/dist/agent/fakeAgent.js +4 -5
  5. package/dist/agent/piAdapter.js +1 -14
  6. package/dist/config/cloneChain.d.ts +12 -0
  7. package/dist/config/cloneChain.js +322 -0
  8. package/dist/config/createPipeline.js +101 -21
  9. package/dist/config/createStage.js +7 -0
  10. package/dist/config/loadOutcome.d.ts +1 -0
  11. package/dist/config/loadPipeline.d.ts +2 -0
  12. package/dist/config/loadPipeline.js +34 -22
  13. package/dist/config/loadStage.d.ts +2 -0
  14. package/dist/config/loadStage.js +135 -125
  15. package/dist/config/migrateYaml.js +14 -3
  16. package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
  17. package/dist/config/normalizePipelineStageEntry.js +133 -60
  18. package/dist/config/parsePreEmitChecks.js +2 -2
  19. package/dist/config/pipelineNeeds.d.ts +15 -2
  20. package/dist/config/pipelineNeeds.js +86 -4
  21. package/dist/config/pipelineRoute.d.ts +46 -0
  22. package/dist/config/pipelineRoute.js +215 -0
  23. package/dist/config/pipelineStageKeys.d.ts +2 -0
  24. package/dist/config/pipelineStageKeys.js +15 -4
  25. package/dist/config/printTargetYaml.js +18 -12
  26. package/dist/config/resolveForkEmitContext.d.ts +1 -1
  27. package/dist/config/resolveForkEmitContext.js +3 -26
  28. package/dist/config/resolvePipelineDag.d.ts +1 -3
  29. package/dist/config/resolvePipelineDag.js +143 -186
  30. package/dist/config/routeIf.d.ts +24 -0
  31. package/dist/config/routeIf.js +247 -0
  32. package/dist/config/validateCatalog.d.ts +1 -1
  33. package/dist/config/validateCatalog.js +5 -3
  34. package/dist/config/yamlDialect.d.ts +5 -8
  35. package/dist/config/yamlDialect.js +37 -29
  36. package/dist/envelope/check.d.ts +0 -2
  37. package/dist/envelope/check.js +1 -57
  38. package/dist/envelope/feedbackLoop.js +0 -3
  39. package/dist/envelope/forkChoice.js +1 -1
  40. package/dist/envelope/payloadSchema.d.ts +14 -0
  41. package/dist/envelope/payloadSchema.js +87 -2
  42. package/dist/mcp/catalogTools.js +1 -2
  43. package/dist/package-meta.d.ts +1 -1
  44. package/dist/package-meta.js +1 -1
  45. package/dist/projection/projectRun.d.ts +0 -2
  46. package/dist/projection/projectRun.js +0 -3
  47. package/dist/prompt/priorEnvelope.js +1 -1
  48. package/dist/runstore/stageInstanceId.d.ts +1 -0
  49. package/dist/runstore/stageInstanceId.js +9 -0
  50. package/dist/runtime/cloneSchedule.d.ts +16 -33
  51. package/dist/runtime/cloneSchedule.js +155 -236
  52. package/dist/runtime/envelopeRouting.js +43 -94
  53. package/dist/runtime/forkGeneration.d.ts +2 -2
  54. package/dist/runtime/forkGeneration.js +29 -15
  55. package/dist/runtime/joinReadiness.d.ts +16 -0
  56. package/dist/runtime/joinReadiness.js +156 -0
  57. package/dist/runtime/pipelineScheduler.d.ts +0 -3
  58. package/dist/runtime/pipelineScheduler.js +81 -65
  59. package/dist/runtime/routeIfEval.d.ts +3 -0
  60. package/dist/runtime/routeIfEval.js +94 -0
  61. package/dist/runtime/runRetryCoordinator.js +0 -4
  62. package/dist/runtime/stageAttemptBootstrap.js +60 -11
  63. package/dist/runtime/taskInput.js +1 -13
  64. package/dist/tools/emitStageEnvelope.d.ts +2 -3
  65. package/dist/tools/emitStageEnvelope.js +1 -65
  66. package/dist/types/envelope.d.ts +0 -2
  67. package/dist/types/forkChoice.d.ts +0 -16
  68. package/dist/types/pipeline.d.ts +86 -27
  69. package/dist/types/stage.d.ts +11 -7
  70. package/dist/ui/assets/{index-D2Wt8hwH.css → index-DsgVjLUS.css} +1 -1
  71. package/dist/ui/assets/{index-XWJovnp7.js → index-DwKGaQvQ.js} +13 -12
  72. package/dist/ui/index.html +2 -2
  73. package/package.json +2 -1
  74. package/skills/stageflow-author/SKILL.md +4 -4
  75. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
  76. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
  77. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
  78. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
  79. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
  80. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
  81. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
  82. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
  83. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
  84. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
  85. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
  86. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
  87. package/skills/stageflow-author/references/catalog-mapping.md +159 -50
  88. package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
  89. package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
  90. package/skills/stageflow-session-capture/SKILL.md +2 -2
  91. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
  92. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
  93. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
  94. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
  95. package/dist/envelope/cloneForks.d.ts +0 -2
  96. package/dist/envelope/cloneForks.js +0 -77
@@ -1,10 +1,10 @@
1
1
  # Stage prompt template
2
2
 
3
- Every external stage file has `id` and `system_prompt`. Filename stem matches `id`. `model` is optional when the pipeline or `stageflow.yaml` supplies it — otherwise set it on the stage. Default when writing a concrete string is `anthropic/claude-sonnet-4-5`. When the human names a different model, write that string verbatim. Before writing models, confirm at least one provider is `configured` (see the skill Provider gate). Sibling stages after a fork should use the same configured model family unless the human asks otherwise.
3
+ Every external stage file has `id`, `system_prompt`, `io.input.schema`, and `io.output.schema`. Filename stem matches `id`. `model` is optional when the pipeline or `stageflow.yaml` supplies it — otherwise set it on the stage. Default when writing a concrete string is `anthropic/claude-sonnet-4-5`. When the human names a different model, write that string verbatim. Before writing models, confirm at least one provider is `configured` (see the skill Provider gate). Sibling stages after fan-out should use the same configured model family unless the human asks otherwise.
4
4
 
5
5
  ## Base
6
6
 
7
- Use for every stage that is not a review point and does not decide a branch.
7
+ Use for every stage that is not a review point. Branching is catalog `if` on the source `route`, not an agent choice. Base stages do not name successor ids.
8
8
 
9
9
  ```yaml
10
10
  id: <id>
@@ -18,6 +18,13 @@ system_prompt: |
18
18
  attempt must be emit_stage_envelope (or ask_operator if waiting on HITL).
19
19
  An empty final message is a stage failure.
20
20
  model: anthropic/claude-sonnet-4-5
21
+ io:
22
+ input:
23
+ schema:
24
+ type: object
25
+ output:
26
+ schema:
27
+ type: object
21
28
  ```
22
29
 
23
30
  The prompt has no `ask_operator` line. It ends on the `emit_stage_envelope` instruction. Call `emit_stage_envelope` once per attempt.
@@ -46,7 +53,7 @@ Set artifacts: [] on emit. Put that path in payload.<field>.
46
53
 
47
54
  ## Summary and payload
48
55
 
49
- Add to every stage (base, gated, or fork):
56
+ Add to every stage (base, gated, or route-if):
50
57
 
51
58
  ```
52
59
  summary and payload are handed to the next stage verbatim. State outcomes and
@@ -75,6 +82,13 @@ system_prompt: |
75
82
  4. Call emit_stage_envelope with an advancing success status only after the
76
83
  operator accepts. Never emit before accept.
77
84
  model: anthropic/claude-sonnet-4-5
85
+ io:
86
+ input:
87
+ schema:
88
+ type: object
89
+ output:
90
+ schema:
91
+ type: object
78
92
  ```
79
93
 
80
94
  Use the `gate_kinds` value that matches the human's review: `artifact_backed` for a file to accept, `confirm` for yes/no, `free_text` for an open reply, `multi_question` for a batch. `ask_operator` does not complete the stage.
@@ -87,28 +101,54 @@ same attempt (commit, push, open PR as required), write any final artifact,
87
101
  then emit. Do not pause after accept. Do not ask_operator for emit/schema errors.
88
102
  ```
89
103
 
90
- ## Fork
104
+ ## Route if
91
105
 
92
- When the pipeline entry has `fork`, the success emit names immediate successors in `fork_choice`. Add this block to the base (or gated) prompt:
106
+ When the source `route` uses `if`, the success payload must include the required discriminator field(s) named in `if.field`. Those fields must be required in `io.output.schema`. The agent emits the field values; it does not name successor ids and does not emit `fork_choice`. Listed `to:` stay on the DAG; `if` decides which run.
107
+
108
+ Add this block to the base (or gated) prompt:
109
+
110
+ ```
111
+ On success, emit the payload fields this stage's route if predicates read
112
+ (name them). Use the types in io.output.schema. Do not name successor stage
113
+ ids. Catalog if decides which children run.
114
+ ```
115
+
116
+ ## Loop
117
+
118
+ When `route` has `{ type: loop }`, the success emit includes envelope field `feedback_loop`: `{ action: continue }` or `{ action: send_back, target: <ancestor id> }`. Catalog YAML uses `{ type: loop }` inside `route`, not a `feedback_loop:` key. `if` is not allowed on the loop entry.
119
+
120
+ Add this block to the base (or gated) prompt:
93
121
 
94
122
  ```
95
- On a success emit, include fork_choice naming immediate successor id(s) that
96
- should run. Name only ids listed as this stage's children. select: one →
97
- exactly one id. select: subset → one or more of those ids.
98
- Normalize free_text answers before mapping (trim, lowercase, collapse spaces
99
- and hyphens). If unmapped, ask_operator again; do not emit.
123
+ On success, include feedback_loop: { action: continue } to advance, or
124
+ { action: send_back, target: <ancestor id> } to replay that ancestor.
100
125
  ```
101
126
 
102
- `fork_choice` on a failure emit is ignored. Leave `clone_forks` unset unless the human explicitly asked for clonable fan-out (see [`catalog-mapping.md`](catalog-mapping.md)).
127
+ ## Clone Chain
128
+
129
+ A Clone Chain emitter succeeds by emitting a valid payload only. Do not emit `clone_forks`. The Clone Array field in `io.output.schema` is the list; Stageflow mints one Clone Instance per element.
103
130
 
104
- ## Clonable parent
131
+ The clone child's opening input is that array element (the named `$ref`) and nothing else. Prompt it as work on one item.
105
132
 
106
- When a successor is `clonable: true`, the parent success emit uses `clone_forks` (not `fork_choice`) for that successor. Add:
133
+ The Join receives the Clone Instance success envelopes (array order). It does not unpack a clone list from the emitter.
134
+
135
+ Add this block to an **emitter** prompt:
136
+
137
+ ```
138
+ On success, emit the list in the Clone Array field named in io.output.schema
139
+ (length 1..clone_cap). Do not emit clone_forks. Stageflow starts one run of
140
+ the next stage per element.
141
+ ```
142
+
143
+ Add this block to a **clone child** prompt:
107
144
 
108
145
  ```
109
- On success, include clone_forks covering every clonable successor exactly once.
110
- Each clone entry is a full envelope with status, summary, artifacts, and payload
111
- matching that successor's io.input.schema — not a stub object.
146
+ Your input is one list element. Do the work for that element only. Emit a
147
+ payload that matches io.output.schema. Do not emit a Clone Array.
112
148
  ```
113
149
 
114
- Clonable child stages use the base (or gated) prompt. Remind them they are one clone among others and must emit independently.
150
+ Add this block to a **Join** prompt:
151
+
152
+ ```
153
+ You receive every Clone Instance success envelope. Gather them in list order.
154
+ ```
@@ -74,8 +74,8 @@ Read [`references/catalog-authoring.md`](references/catalog-authoring.md) before
74
74
  `node scripts/resolve-catalog-id.mjs --text "<phrase>" --dir <write-dir> --kind pipeline|stage`
75
75
 
76
76
  Use the printed `{"id":"..."}`.
77
- 3. Write one external stage YAML per phase (`id`, `system_prompt`, optional `model`). End every `system_prompt` with an `emit_stage_envelope` footer. Omit `gate_kinds`. Filename stem must match `id`.
78
- 4. Write one pipeline YAML that wires the stages with `uses:` and `needs:` in dependency order.
77
+ 3. Write one external stage YAML per phase (`id`, `system_prompt`, `io`, optional `model`). End every `system_prompt` with an `emit_stage_envelope` footer. Omit `gate_kinds`. Filename stem must match `id`.
78
+ 4. Write one pipeline YAML that wires the stages with `uses:`, marks the first stage `entry: true`, and lists forward `route: [{ to: next }]` on each non-leaf in dependency order.
79
79
 
80
80
  Do not write a `*.task.yaml`.
81
81
 
@@ -3,6 +3,8 @@ model: anthropic/claude-sonnet-4-5
3
3
  stages:
4
4
  - id: research
5
5
  uses: ./research.yaml
6
+ entry: true
7
+ route:
8
+ - to: implement
6
9
  - id: implement
7
10
  uses: ./implement.yaml
8
- needs: [research]
@@ -7,6 +7,21 @@ system_prompt: |
7
7
  and a payload describing what you produced. Your last tool call in this
8
8
  attempt must be emit_stage_envelope. An empty final message is a stage failure.
9
9
  io:
10
+ input:
11
+ schema:
12
+ type: object
13
+ required:
14
+ - known
15
+ - unknown
16
+ properties:
17
+ known:
18
+ type: array
19
+ items:
20
+ type: string
21
+ unknown:
22
+ type: array
23
+ items:
24
+ type: string
10
25
  output:
11
26
  schema:
12
27
  type: object
@@ -7,6 +7,9 @@ system_prompt: |
7
7
  and a payload the next stage can use. Your last tool call in this attempt must
8
8
  be emit_stage_envelope. An empty final message is a stage failure.
9
9
  io:
10
+ input:
11
+ schema:
12
+ type: object
10
13
  output:
11
14
  schema:
12
15
  type: object
@@ -10,7 +10,7 @@ Resolve collision-safe ids with `scripts/resolve-catalog-id.mjs` (`--text`, `--d
10
10
 
11
11
  ## External stage file
12
12
 
13
- Required fields: `id`, `system_prompt`. Filename stem must match `id` (`research.yaml` → `id: research`). `model` is optional when the pipeline or manifest supplies it.
13
+ Required fields: `id`, `system_prompt`, `io.input.schema`, `io.output.schema`. Filename stem must match `id` (`research.yaml` → `id: research`). `model` is optional when the pipeline or manifest supplies it.
14
14
 
15
15
  Every `system_prompt` ends with a mandatory `emit_stage_envelope` footer, for example:
16
16
 
@@ -28,7 +28,9 @@ Worked example: `assets/example-pipeline/research.yaml` and `assets/example-pipe
28
28
 
29
29
  ## Pipeline file
30
30
 
31
- `id` plus `stages:` entries with `id`, `uses:` (path relative to the pipeline file), and `needs:` for every non-root stage. Linear chain: each stage `needs` the previous id (scalar or a one-item array). Optional pipeline-root `model` fills stages that omit `model`. Put body contracts (`io`, `verify`) on stage files; put `on_verify_fail` on the pipeline entry when after-phase verify should repair or wait for an operator.
31
+ `id` plus `stages:` array of object entries with `id`, `uses:` (path relative to the pipeline file). Mark the first stage `entry: true`. Linear chain: each non-leaf lists `route: [{ to: next-id }]`. Optional pipeline-root `model` fills stages that omit `model`. Put body contracts (`io`, `verify`) on stage files; `io.input.schema` and `io.output.schema` are required on every stage. Put `on_verify_fail` on the pipeline entry when after-phase verify should repair or wait for an operator.
32
+
33
+ Session-capture is a linear chain of transcript phases. Exclusive branches, if needed, are catalog `if` on `route` after success, not agent-picked ids — see [`../../stageflow-author/references/catalog-mapping.md`](../../stageflow-author/references/catalog-mapping.md). `needs` and `fork` fail load.
32
34
 
33
35
  Worked example: `assets/example-pipeline/example.pipeline.yaml`.
34
36
 
@@ -1,2 +0,0 @@
1
- import { type CloneEmitContext } from "../types/forkChoice.js";
2
- export declare function assertCloneForks(params: unknown, cloneEmitContext: CloneEmitContext): void;
@@ -1,77 +0,0 @@
1
- import { parseCloneForks } from "./check.js";
2
- import { assertCloneAssignmentPayload } from "./payloadSchema.js";
3
- import { EnvelopeError } from "../types/envelope.js";
4
- import { CLONE_ACTIONS } from "../types/forkChoice.js";
5
- function fieldPresent(item, key) {
6
- return item[key] !== undefined;
7
- }
8
- export function assertCloneForks(params, cloneEmitContext) {
9
- const record = params;
10
- if (record.status === "failure")
11
- return;
12
- if (record.clone_forks === undefined || !Array.isArray(record.clone_forks)) {
13
- throw new EnvelopeError("clonable successor requires clone_forks on success");
14
- }
15
- const items = parseCloneForks(record.clone_forks);
16
- if (items.length === 0 && cloneEmitContext.clonableSuccessors.length > 0) {
17
- throw new EnvelopeError("clone_forks must not be empty when clonable successors exist");
18
- }
19
- const expectedIds = cloneEmitContext.clonableSuccessors.map((s) => s.successorId);
20
- const expectedSet = new Set(expectedIds);
21
- const seen = new Set();
22
- for (const [i, item] of items.entries()) {
23
- const itemPath = `clone_forks[${i}]`;
24
- if (seen.has(item.successor_id)) {
25
- throw new EnvelopeError(`${itemPath} contains duplicate successor_id: ${item.successor_id}`);
26
- }
27
- if (!expectedSet.has(item.successor_id)) {
28
- throw new EnvelopeError(`${itemPath} contains extra successor_id: ${item.successor_id}`);
29
- }
30
- seen.add(item.successor_id);
31
- const capEntry = cloneEmitContext.clonableSuccessors.find((s) => s.successorId === item.successor_id);
32
- const cloneCap = capEntry?.cloneCap;
33
- const allowedActions = cloneEmitContext.allowedActions ?? [...CLONE_ACTIONS];
34
- if (!allowedActions.includes(item.action)) {
35
- throw new EnvelopeError(`${itemPath} action "${item.action}" is not allowed for ${item.successor_id} (allowed: ${allowedActions.join(", ")})`);
36
- }
37
- if (item.action === "skip") {
38
- if (fieldPresent(item, "envelope") ||
39
- fieldPresent(item, "mode") ||
40
- fieldPresent(item, "clones")) {
41
- throw new EnvelopeError(`${itemPath} skip for ${item.successor_id} must not include envelope, mode, or clones`);
42
- }
43
- }
44
- else if (item.action === "once") {
45
- if (!fieldPresent(item, "envelope")) {
46
- throw new EnvelopeError(`${itemPath} once for ${item.successor_id} requires envelope`);
47
- }
48
- if (fieldPresent(item, "mode") || fieldPresent(item, "clones")) {
49
- throw new EnvelopeError(`${itemPath} once for ${item.successor_id} must not include mode or clones`);
50
- }
51
- if (capEntry?.cloneInputSchema !== undefined) {
52
- assertCloneAssignmentPayload(item.envelope, capEntry.cloneInputSchema, item.successor_id, `${itemPath}.envelope`);
53
- }
54
- }
55
- else if (item.action === "fanout") {
56
- if (fieldPresent(item, "envelope")) {
57
- throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} must not include a top-level envelope`);
58
- }
59
- if (!fieldPresent(item, "mode") || !fieldPresent(item, "clones") || item.clones === undefined) {
60
- throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} requires mode and clones`);
61
- }
62
- if (cloneCap === undefined || item.clones.length < 2 || item.clones.length > cloneCap) {
63
- throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} must have between 2 and ${cloneCap} clones`);
64
- }
65
- if (capEntry?.cloneInputSchema !== undefined) {
66
- for (const [j, clone] of item.clones.entries()) {
67
- assertCloneAssignmentPayload(clone.envelope, capEntry.cloneInputSchema, item.successor_id, `${itemPath}.clones[${j}].envelope`);
68
- }
69
- }
70
- }
71
- }
72
- for (const id of expectedIds) {
73
- if (!seen.has(id)) {
74
- throw new EnvelopeError(`clone_forks missing successor_id: ${id}`);
75
- }
76
- }
77
- }