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
@@ -13,8 +13,8 @@
13
13
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap" />
14
14
  <link rel="icon" type="image/svg+xml" href="/stageflow-icon.svg" />
15
15
  <title>Stageflow</title>
16
- <script type="module" crossorigin src="/assets/index-GtyiWiZg.js"></script>
17
- <link rel="stylesheet" crossorigin href="/assets/index-uVHiae8s.css">
16
+ <script type="module" crossorigin src="/assets/index-DwKGaQvQ.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-DsgVjLUS.css">
18
18
  </head>
19
19
  <body>
20
20
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stageflow",
3
- "version": "0.15.0",
3
+ "version": "0.18.0",
4
4
  "description": "Stageflow — CLI pipeline runtime for configurable stages on Pi",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "test": "vitest run",
38
38
  "test:watch": "vitest",
39
39
  "dev": "tsx src/cli.ts",
40
+ "dev:watch": "PI_CURSOR_SETTING_SOURCES=all STAGEFLOW_ACTIVITY_VERBOSE=1 tsx watch src/cli.ts ui",
40
41
  "typecheck": "tsc -p tsconfig.json --noEmit",
41
42
  "ui:dev": "npm run dev -w ui",
42
43
  "ui:build": "npm run build -w ui && npm run copy:ui-dist",
@@ -37,7 +37,7 @@ Ask until you can name every step, how they sequence, any decision point, and an
37
37
 
38
38
  Read [`references/catalog-mapping.md`](references/catalog-mapping.md). Apply those rules to the confirmed structure. Read [`references/stage-prompt-template.md`](references/stage-prompt-template.md) for each stage's `system_prompt`, optional `model`, `io`, `verify`, and `gate_kinds`. Apply the Anti-patterns section below while mapping. When the human needs hard advance gates, plan body `verify` and wiring `on_verify_fail`. When a review can demand changes, include an address-feedback stage before approve/ship.
39
39
 
40
- **Done when** you have a pipeline id, one stage id per step, `needs` / `fork` wiring, a prompt plan per stage, and any `verify` / `on_verify_fail` / remediation stages the structure requires.
40
+ **Done when** you have a pipeline id, one stage id per step, `route` / `entry` wiring (optional `if`, `{ type: loop }`, sealed Clone Chain when the human wants one run per list item), a prompt plan per stage, and any `verify` / `on_verify_fail` / remediation stages the structure requires.
41
41
 
42
42
  ## Anti-patterns
43
43
 
@@ -45,15 +45,15 @@ Author these contracts explicitly. They come from long runs that failed after ho
45
45
 
46
46
  | Always do | How |
47
47
  |---|---|
48
- | End every attempt on a tool call | Prompt: last call is `emit_stage_envelope`, or `ask_operator` while waiting on HITL. Each clonable instance emits on its own. |
48
+ | End every attempt on a tool call | Prompt: last call is `emit_stage_envelope`, or `ask_operator` while waiting on HITL. |
49
49
  | Land required files in the attempt artifact dir | Prompt `write_stage_artifact`. Body `verify` with `type: artifact` and `when` including `after`. `ask_operator` `artifact_backed` references that same path. |
50
50
  | Make implement stages change the checkout | Body `verify` `checkout_changes` + `path_fields`; `io.output.schema` with required checkout-relative path arrays. Exploration-only is its own stage or a failure emit. |
51
51
  | Hand successors clean envelopes | `summary` / `payload` = outcomes and artifact pointers only. Name required `io.output.schema` fields in the prompt. |
52
52
  | Give review blockers a place to land | Wire `review → address-feedback → approve/ship`. Put `verdict` / `blocking_findings` (or equivalent) in the review `io.output.schema`. |
53
53
  | Bind success to the full checklist | If a plan lists N commands, success requires all of them in `commands_run` with exit 0. Cheap checks first; slow suites last or in a verify stage. Size `timeout_ms` to the work. |
54
- | Use configured models on every sibling | Same configured `model` family after a fork/fan-out unless the human asks otherwise. Reliable model on publish/PR stages. |
54
+ | Use configured models on every sibling | Same configured `model` family after fan-out unless the human asks otherwise. Reliable model on publish/PR stages. |
55
55
  | Finish publish in the accept attempt | After `artifact_backed` accept: side effects (commit/push/PR) → final artifact → emit in the same attempt. Fix emit/schema errors by re-emitting. |
56
- | Emit full clone assignments | When `clonable: true`, parent success uses `clone_forks` with full envelopes matching `io.input.schema`. Leave clonable unset when the clone count is unknown. |
56
+ | Author a sealed Clone Chain | One run per list item is emitter → clone child → Join. Never write `clonable`, `clone_forks`, or emit-time `skip` / `once` / `fanout`. See [YAML catalog — Clone Chain](../../docs/yaml-catalog.md#clone-chain). |
57
57
 
58
58
  Throwaway checkout deliverables (HTML spike, scratch file): prompt builtin `write` at a checkout-relative path, `artifacts: []` on emit, path in `payload`.
59
59
 
@@ -1,13 +1,21 @@
1
1
  id: hotfix
2
2
  system_prompt: |
3
- Take the failing results from the prior envelope and repair the work they
4
- describe.
3
+ Take the prior envelope (payload.ready is false) and repair the work it
4
+ describes.
5
5
 
6
6
  Write the repair notes via write_stage_artifact (for example hotfix.md).
7
7
 
8
8
  When finished, call emit_stage_envelope exactly once with status, summary,
9
9
  artifacts, and a payload describing what you repaired.
10
10
  io:
11
+ input:
12
+ schema:
13
+ type: object
14
+ required:
15
+ - ready
16
+ properties:
17
+ ready:
18
+ type: boolean
11
19
  output:
12
20
  schema:
13
21
  type: object
@@ -3,14 +3,22 @@ model: anthropic/claude-sonnet-4-5
3
3
  stages:
4
4
  - id: run-tests
5
5
  uses: ./run-tests.yaml
6
- fork:
7
- select: one
6
+ entry: true
7
+ route:
8
+ - to: ship
9
+ if:
10
+ field: ready
11
+ op: eq
12
+ value: true
13
+ - to: hotfix
14
+ if:
15
+ field: ready
16
+ op: eq
17
+ value: false
8
18
  - id: hotfix
9
19
  uses: ./hotfix.yaml
10
- needs: [run-tests]
11
20
  - id: ship
12
21
  uses: ./ship.yaml
13
- needs: [run-tests]
14
22
  on_verify_fail:
15
23
  mode: manual
16
24
  retry_safety: side_effecting
@@ -4,13 +4,15 @@ system_prompt: |
4
4
 
5
5
  Write the results via write_stage_artifact (for example results.md).
6
6
 
7
+ When you can assess readiness, emit a success envelope. Set payload.ready to
8
+ true or false. Do not emit a failure status to express unreadiness.
9
+
7
10
  When finished, call emit_stage_envelope exactly once with status, summary,
8
11
  artifacts, and a payload the next stage can use.
9
-
10
- On a success emit, include fork_choice naming immediate successor id(s) that
11
- should run. Name only ids listed as this stage's children. select: one →
12
- exactly one id: hotfix when the results fail, ship when they pass.
13
12
  io:
13
+ input:
14
+ schema:
15
+ type: object
14
16
  output:
15
17
  schema:
16
18
  type: object
@@ -1,6 +1,6 @@
1
1
  id: ship
2
2
  system_prompt: |
3
- Take the passing results from the prior envelope and produce the shippable
3
+ Take the prior envelope (payload.ready is true) and produce the shippable
4
4
  package.
5
5
 
6
6
  Write the package notes via write_stage_artifact (for example ship.md).
@@ -8,6 +8,14 @@ system_prompt: |
8
8
  When finished, call emit_stage_envelope exactly once with status, summary,
9
9
  artifacts, and a payload describing what you shipped.
10
10
  io:
11
+ input:
12
+ schema:
13
+ type: object
14
+ required:
15
+ - ready
16
+ properties:
17
+ ready:
18
+ type: boolean
11
19
  output:
12
20
  schema:
13
21
  type: object
@@ -9,6 +9,9 @@ system_prompt: |
9
9
  attempt must be emit_stage_envelope (or ask_operator if waiting on HITL).
10
10
  An empty final message is a stage failure.
11
11
  io:
12
+ input:
13
+ schema:
14
+ type: object
12
15
  output:
13
16
  schema:
14
17
  type: object
@@ -8,6 +8,14 @@ system_prompt: |
8
8
  When finished, call emit_stage_envelope exactly once with status, summary,
9
9
  artifacts, and a payload describing what you produced.
10
10
  io:
11
+ input:
12
+ schema:
13
+ type: object
14
+ required:
15
+ - verdict
16
+ properties:
17
+ verdict:
18
+ type: string
11
19
  output:
12
20
  schema:
13
21
  type: object
@@ -3,9 +3,13 @@ model: anthropic/claude-sonnet-4-5
3
3
  stages:
4
4
  - id: draft
5
5
  uses: ./draft.yaml
6
+ entry: true
7
+ route:
8
+ - to: review
6
9
  - id: review
7
10
  uses: ./review.yaml
8
- needs: [draft]
11
+ route:
12
+ - to: publish
9
13
  on_verify_fail:
10
14
  mode: repair
11
15
  max_attempts: 3
@@ -13,7 +17,6 @@ stages:
13
17
  include_failed_checks: true
14
18
  - id: publish
15
19
  uses: ./publish.yaml
16
- needs: [review]
17
20
  on_verify_fail:
18
21
  mode: manual
19
22
  retry_safety: side_effecting
@@ -13,6 +13,14 @@ system_prompt: |
13
13
  4. Call emit_stage_envelope with an advancing success status only after the
14
14
  operator accepts. Never emit before accept.
15
15
  io:
16
+ input:
17
+ schema:
18
+ type: object
19
+ required:
20
+ - title
21
+ properties:
22
+ title:
23
+ type: string
16
24
  output:
17
25
  schema:
18
26
  type: object
@@ -7,6 +7,9 @@ system_prompt: |
7
7
  When finished, call emit_stage_envelope exactly once with status, summary,
8
8
  artifacts, and a payload the next stage can use.
9
9
  io:
10
+ input:
11
+ schema:
12
+ type: object
10
13
  output:
11
14
  schema:
12
15
  type: object
@@ -3,12 +3,15 @@ model: anthropic/claude-sonnet-4-5
3
3
  stages:
4
4
  - id: gather
5
5
  uses: ./gather.yaml
6
+ entry: true
7
+ route:
8
+ - to: summarize
6
9
  - id: summarize
7
10
  uses: ./summarize.yaml
8
- needs: [gather]
11
+ route:
12
+ - to: send
9
13
  - id: send
10
14
  uses: ./send.yaml
11
- needs: [summarize]
12
15
  on_verify_fail:
13
16
  mode: manual
14
17
  retry_safety: side_effecting
@@ -8,6 +8,14 @@ system_prompt: |
8
8
  When finished, call emit_stage_envelope exactly once with status, summary,
9
9
  artifacts, and a payload describing who received the digest.
10
10
  io:
11
+ input:
12
+ schema:
13
+ type: object
14
+ required:
15
+ - headline
16
+ properties:
17
+ headline:
18
+ type: string
11
19
  output:
12
20
  schema:
13
21
  type: object
@@ -7,6 +7,14 @@ system_prompt: |
7
7
  When finished, call emit_stage_envelope exactly once with status, summary,
8
8
  artifacts, and a payload the next stage can use.
9
9
  io:
10
+ input:
11
+ schema:
12
+ type: object
13
+ required:
14
+ - source_count
15
+ properties:
16
+ source_count:
17
+ type: integer
10
18
  output:
11
19
  schema:
12
20
  type: object
@@ -16,21 +16,67 @@ Collision handling lives in [`catalog-write-conventions.md`](catalog-write-conve
16
16
 
17
17
  ## Sequencing
18
18
 
19
- `needs` is a parent stage id, or a non-empty array (length ≥ 1). Strings default to `on: [succeeded]`. `{ id, on }` accepts a non-empty unique subset of `succeeded` | `failed` | `skipped`.
19
+ Write **Author YAML only**:
20
+
21
+ - Wiring: `route` / `entry: true` / `{ type: loop }`
22
+ - Contracts: `io` / `verify` / `on_verify_fail`
23
+ - Every stage body has `io.input.schema` and `io.output.schema`
24
+ - Sequential and fan-in edges: consumer `io.input` must be a structural subset of **each** parent's `io.output`
25
+
26
+ Wiring is declared on the **source** stage. Children do not list parents. A pipeline that uses `route` must mark at least one `entry: true` root.
27
+
28
+ `route` is a list of entries. Forward entries name `to:`, optional `on:`, optional `if`. Load rejects `needs`, `fork`, `feedback_loop`, `route_select`, and `allow_none`.
20
29
 
21
30
  | Human says | Pipeline shape |
22
31
  |---|---|
23
- | Steps in order | Each later stage `needs` the previous id. No `fork` field. |
24
- | Steps happen together | Sibling stages share one `needs` (the same parent). No `fork` field. |
25
- | A later step waits for two or more earlier steps | That stage `needs` an array of those parent ids (length ≥ 2). |
26
- | Exactly one branch runs | Deciding stage gets `fork: { select: one }`. Each branch `needs` the decider. |
27
- | Either, both, or a subset may run | Deciding stage gets `fork: { select: subset }`. Each branch `needs` the decider. |
32
+ | Steps in order | First stage `entry: true`. Each source lists `route: [{ to: next }]`. |
33
+ | Steps happen together | Parent lists multiple `to:` with no `if`. Both run after the parent succeeds. |
34
+ | A later step waits for two or more earlier steps | Each parent lists `to: join`. The join does not declare parents. |
35
+ | Exactly one of several successors should run | Source lists every `to:` with mutually exclusive `if`s on a required payload field. Agent emits that field; it does not name successor ids. |
36
+ | Optional extra successor | `if` on that `to:`. Keep an ungated sibling if something should always run. |
37
+ | Review can send work back | Source `route` includes `{ type: loop, to: ancestor, max_replays, on_max_replays, replay_session }`. Success emit includes envelope `feedback_loop`. |
38
+ | One run per list item | Sealed [Clone Chain](../../../docs/yaml-catalog.md#clone-chain): emitter → clone child → Join. Emitter output has exactly one array of a named `$ref`; the clone child's entire input is that `$ref`. Emitter pipeline entry has `clone_cap` and `clone_mode`. |
28
39
 
29
- Default `select` is `one` unless the human says more than one branch can run.
40
+ A review, approval, or sign-off step is a gated stage: put `gate_kinds` on that stage file and follow [`stage-prompt-template.md`](stage-prompt-template.md).
30
41
 
31
- Map linear chains, sibling fan-out, generic fan-in (`needs` array), and single-level `fork`.
42
+ ### `on:` (skip-cascade policy, not a launch)
32
43
 
33
- A review, approval, or sign-off step is a gated stage: put `gate_kinds` on that stage file and follow [`stage-prompt-template.md`](stage-prompt-template.md).
44
+ Default is succeeded-only. Omit `on:` unless you need to opt an edge out of skip-cascade.
45
+
46
+ Including `failed` or `skipped` in `on:` opts that edge out of skip-cascade. It does **not** launch a child from a skipped or failed parent.
47
+
48
+ - Single-parent: launch still requires a **succeeded** parent. A skipped parent skip-cascades children whose `on` does **not** include `skipped`. Including `skipped` only prevents cascade; the child stays pending.
49
+ - Multi-parent Join (two or more parents each listing `to:` the same child): never skip-cascaded from one parent. Wait until every parent is terminal. Runs if at least one parent succeeded (skipped siblings do not block). Stays pending if any parent failed (even if `on` lists `failed`). Force-skipped if every parent skipped.
50
+
51
+ For exclusive branching after a check, emit **success** with a required discriminator in `io.output.schema` and gate successors with `if`. `on: [failed]` does not mean “run hotfix when tests fail” — that child will not launch.
52
+
53
+ ### `if` (runtime gate after success)
54
+
55
+ Evaluated after the source **succeeds**, against that source's output payload only.
56
+
57
+ Leaf `{ field, op, value }` or composition `{ all: [...] }` | `{ any: [...] }` | `{ not: {...} }`.
58
+
59
+ Operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`. Type-strict (`1` is not `"1"`).
60
+
61
+ `field` is a dot-separated path; every segment must be a **required** object property in the source `io.output.schema`.
62
+
63
+ Two matching `if`s both fire (not first-match-wins).
64
+
65
+ When every forward `to:` on a stage has `if`, validate warns `pipeline.route_all_gated` (`ok: true`; `--strict` does not promote). That is acceptable for exclusive branches.
66
+
67
+ `if` is illegal on `{ type: loop }`, or combined with `on` other than succeeded-only.
68
+
69
+ The completing agent does **not** pick which successors run. Do not emit `fork_choice`.
70
+
71
+ ### Loops
72
+
73
+ A review that can send work back to an ancestor is a `{ type: loop }` entry **inside** `route` on the source (the reviewer). Required fields: `type: loop`, `to` (earlier ancestor), `max_replays`, `on_max_replays` (`require_continue` | `wait_for_human`), `replay_session` (`resume` | `new_session`).
74
+
75
+ The **envelope** field on emit is still `feedback_loop` (`continue` / `send_back`). Catalog YAML does not use a `feedback_loop:` key.
76
+
77
+ Optional `replay_safe: false` on stages that must not be replayed (e.g. one-shot submit).
78
+
79
+ Forward-only “review → address-feedback → approve” is still the right map when the human wants a separate fix stage rather than send-back.
34
80
 
35
81
  ## Review then fix then approve
36
82
 
@@ -40,7 +86,7 @@ When the human wants a review that can demand changes, then a later approve/ship
40
86
  … → review → address-feedback → approve
41
87
  ```
42
88
 
43
- Do not wire approve/ship as the immediate child of review. Pipelines are forward-only; without an address-feedback stage, blocking findings have nowhere to land. The address-feedback stage is the only post-review editor; the approve stage is the backstop.
89
+ Do not wire approve/ship as the immediate child of review when blockers are expected. The address-feedback stage is the only post-review editor; the approve stage is the backstop. When they want send-back to an earlier stage instead, put `{ type: loop }` on the reviewer's `route`.
44
90
 
45
91
  ## Verify and on_verify_fail
46
92
 
@@ -56,22 +102,21 @@ Put `verify` on the **stage body** (the `uses:` file or inline entry). Put `on_v
56
102
 
57
103
  `on_verify_fail` requires at least one after-phase `verify` item. Check `id` values must be unique within the stage. Check discriminator is `type:` (gate widgets still use `kind:`). Wire these for writer stages and final gates; prompts alone do not enforce them.
58
104
 
59
- ## Clonable successors
105
+ ## Clone Chain
106
+
107
+ A Clone Chain is emitter → clone child → Join. The emitter output has exactly one array of a named `$ref`; the clone child's entire input is that same `$ref`. Put `clone_cap` (integer ≥ 1) and `clone_mode` (`parallel` | `sequential`) on the **emitter pipeline entry**, not on the stage body. N=1 still mints `{child}~1`.
60
108
 
61
- A runtime clone count is not knowable from a vague description — leave `clonable` and `clone_forks` unset and prefer sibling stages or a single review.
109
+ An empty Clone Array or a length above the Clone Cap fails the **emitter emit** — it does not skip the chain or truncate. Gate a zero-item path before the emitter. Both inbound chain edges are sealed: no extra routes, no `if`. The Join may Loop only to a stage before the emitter; the emitter, clone child, and Join are not Loop targets; the clone child cannot Loop or `send_back`.
62
110
 
63
- When the human explicitly wants N parallel instances of **one** successor catalog id (e.g. several review lenses, several prototype variants):
111
+ See [YAML catalog — Clone Chain](../../../docs/yaml-catalog.md#clone-chain) and [`clone-chain-smallest.pipeline.yaml`](../../../tests/fixtures/pipelines/clone-chain-smallest.pipeline.yaml).
64
112
 
65
- - On that successor pipeline entry: `clonable: true` and `clone_cap` (integer ≥ 2).
66
- - That successor must have at least one child (a join / address-feedback / collect stage). It cannot be a DAG leaf.
67
- - Parent success emit uses `clone_forks` for that successor (not `fork_choice`). Each clone assignment is a full envelope; validate assignments with `io.input.schema` on the clonable stage body.
68
- - Join stages that wait on the clonable parent read clone-list `priorEnvelopes`, not `priorEnvelopesByStage`.
113
+ ## Rejected clone fields
69
114
 
70
- `fork` + `fork_choice` picks which **branch stage ids** run. `clone_forks` spawns **N instances** of one successor id.
115
+ Do not author `clonable`, `clone_actions`, or envelope `clone_forks`. Do not put `clone_cap` / `clone_mode` on a stage that is not a Clone Chain emitter.
71
116
 
72
117
  ## Models
73
118
 
74
- Confirm providers before writing. After a fork, give every sibling the same configured `model` unless the human asks for different ones. Prefer a reliable configured model on side-effecting final stages.
119
+ Confirm providers before writing. After fan-out, give every sibling the same configured `model` unless the human asks for different ones. Prefer a reliable configured model on side-effecting final stages.
75
120
 
76
121
  ## Worked examples
77
122
 
@@ -85,12 +130,23 @@ model: anthropic/claude-sonnet-4-5
85
130
  stages:
86
131
  - id: draft
87
132
  uses: ./draft.yaml
133
+ entry: true
134
+ route:
135
+ - to: review
88
136
  - id: review
89
137
  uses: ./review.yaml
90
- needs: [draft]
138
+ route:
139
+ - to: publish
140
+ on_verify_fail:
141
+ mode: repair
142
+ max_attempts: 3
143
+ retry_safety: idempotent
144
+ include_failed_checks: true
91
145
  - id: publish
92
146
  uses: ./publish.yaml
93
- needs: [review]
147
+ on_verify_fail:
148
+ mode: manual
149
+ retry_safety: side_effecting
94
150
  ```
95
151
 
96
152
  `review` carries `gate_kinds: [artifact_backed]` and emit-phase `verify` `type: gate`. Full set: [`../assets/examples/linear-review/`](../assets/examples/linear-review/).
@@ -103,55 +159,78 @@ When review can emit blockers and a later stage approves or ships, insert addres
103
159
  stages:
104
160
  - id: implement
105
161
  uses: ./implement.yaml
162
+ entry: true
163
+ route:
164
+ - to: review
106
165
  - id: review
107
166
  uses: ./review.yaml
108
- needs: [implement]
167
+ route:
168
+ - to: address-feedback
109
169
  - id: address-feedback
110
170
  uses: ./address-feedback.yaml
111
- needs: [review]
171
+ route:
172
+ - to: approve
112
173
  - id: approve
113
174
  uses: ./approve.yaml
114
- needs: [address-feedback]
115
175
  ```
116
176
 
117
- Do not make `approve` / `ship` `needs: review` when blockers are expected.
177
+ Do not wire approve/ship as the immediate child of review when blockers are expected.
118
178
 
119
- ### Release gate (fork, select one)
179
+ ### Release gate (exclusive via `if`)
120
180
 
121
- One deciding step, then exactly one successor.
181
+ `run-tests` emits **success** with required `ready` boolean. Catalog `if` picks the successor. A failure emit would leave both children unlaunched.
122
182
 
123
183
  ```yaml
124
184
  id: release-gate
185
+ model: anthropic/claude-sonnet-4-5
125
186
  stages:
126
187
  - id: run-tests
127
188
  uses: ./run-tests.yaml
128
- fork:
129
- select: one
189
+ entry: true
190
+ route:
191
+ - to: ship
192
+ if:
193
+ field: ready
194
+ op: eq
195
+ value: true
196
+ - to: hotfix
197
+ if:
198
+ field: ready
199
+ op: eq
200
+ value: false
130
201
  - id: hotfix
131
202
  uses: ./hotfix.yaml
132
- needs: [run-tests]
133
203
  - id: ship
134
204
  uses: ./ship.yaml
135
- needs: [run-tests]
205
+ on_verify_fail:
206
+ mode: manual
207
+ retry_safety: side_effecting
136
208
  ```
137
209
 
138
210
  Full set: [`../assets/examples/branch-decision/`](../assets/examples/branch-decision/).
139
211
 
140
212
  ### Research digest (linear, non-software)
141
213
 
142
- Weekly gather → summarize → send. Same `needs` chain as the review loop; ids and prompts stay in that domain.
214
+ Weekly gather → summarize → send. Same `entry` + `route` chain as the review loop; ids and prompts stay in that domain.
143
215
 
144
216
  ```yaml
145
217
  id: research-digest
218
+ model: anthropic/claude-sonnet-4-5
146
219
  stages:
147
220
  - id: gather
148
221
  uses: ./gather.yaml
222
+ entry: true
223
+ route:
224
+ - to: summarize
149
225
  - id: summarize
150
226
  uses: ./summarize.yaml
151
- needs: [gather]
227
+ route:
228
+ - to: send
152
229
  - id: send
153
230
  uses: ./send.yaml
154
- needs: [summarize]
231
+ on_verify_fail:
232
+ mode: manual
233
+ retry_safety: side_effecting
155
234
  ```
156
235
 
157
236
  Full set: [`../assets/examples/non-sdlc-digest/`](../assets/examples/non-sdlc-digest/).
@@ -164,15 +243,17 @@ Full set: [`../assets/examples/non-sdlc-digest/`](../assets/examples/non-sdlc-di
164
243
  stages:
165
244
  - id: intake
166
245
  uses: ./intake.yaml
246
+ entry: true
247
+ route:
248
+ - to: collect-quotes
249
+ - to: collect-notes
167
250
  - id: collect-quotes
168
251
  uses: ./collect-quotes.yaml
169
- needs: [intake]
170
252
  - id: collect-notes
171
253
  uses: ./collect-notes.yaml
172
- needs: [intake]
173
254
  ```
174
255
 
175
- No `fork` field. Both siblings run.
256
+ Both run after intake succeeds. No `if`.
176
257
 
177
258
  ### Diamond join
178
259
 
@@ -182,37 +263,65 @@ No `fork` field. Both siblings run.
182
263
  stages:
183
264
  - id: clarify
184
265
  uses: ./clarify.yaml
266
+ entry: true
267
+ route:
268
+ - to: research
269
+ - to: validation
185
270
  - id: research
186
271
  uses: ./research.yaml
187
- needs: [clarify]
272
+ route:
273
+ - to: synthesize
188
274
  - id: validation
189
275
  uses: ./validation.yaml
190
- needs: [clarify]
276
+ route:
277
+ - to: synthesize
191
278
  - id: synthesize
192
279
  uses: ./synthesize.yaml
193
- needs:
194
- - research
195
- - validation
196
280
  ```
197
281
 
198
- `needs` array length ≥ 2 is keyed fan-in. The join reads `priorEnvelopesByStage`, not clone-list `priorEnvelopes`.
282
+ The join reads `priorEnvelopesByStage`, not a list of sibling envelopes. A skipped sibling does not block. A failed parent leaves the join pending.
199
283
 
200
- ### Fork, select subset
284
+ ### Optional / subset fan-out
201
285
 
202
- "Email, post, or both could go out":
286
+ "Email, post, or both": list both `to:` with no `if` (both run). To skip a channel, add `if` on that `to:` and put the discriminator in `io.output.schema`. Do not emit `fork_choice`.
203
287
 
204
288
  ```yaml
205
289
  stages:
206
290
  - id: choose-channels
207
291
  uses: ./choose-channels.yaml
208
- fork:
209
- select: subset
292
+ entry: true
293
+ route:
294
+ - to: email
295
+ - to: post
210
296
  - id: email
211
297
  uses: ./email.yaml
212
- needs: [choose-channels]
213
298
  - id: post
214
299
  uses: ./post.yaml
215
- needs: [choose-channels]
216
300
  ```
217
301
 
218
- The success emit names one or more of those successor ids in `fork_choice`.
302
+ To skip a channel, add `if` on that `to:` (keep an ungated sibling if the other channel should always run).
303
+
304
+ ### Review send-back (loop)
305
+
306
+ ```yaml
307
+ stages:
308
+ - id: implement
309
+ uses: ./implement.yaml
310
+ entry: true
311
+ route:
312
+ - to: review
313
+ - id: review
314
+ uses: ./review.yaml
315
+ route:
316
+ - to: submit
317
+ - type: loop
318
+ to: implement
319
+ max_replays: 2
320
+ on_max_replays: require_continue
321
+ replay_session: resume
322
+ - id: submit
323
+ uses: ./submit.yaml
324
+ replay_safe: false
325
+ ```
326
+
327
+ Success emit from `review` includes envelope `feedback_loop`: `{ action: continue }` or `{ action: send_back, target: implement }`.
@@ -17,10 +17,10 @@ One directory (or the project root) holds:
17
17
 
18
18
  | File | Required fields |
19
19
  |---|---|
20
- | `<pipeline-id>.pipeline.yaml` | `id` matching the filename stem; `stages:` object entries |
21
- | `<stage-id>.yaml` | `id`, `system_prompt`; `model` when not inherited; `io` / `verify` / `gate_kinds` when the step needs them |
20
+ | `<pipeline-id>.pipeline.yaml` | `id` matching the filename stem; `stages:` array of object entries |
21
+ | `<stage-id>.yaml` | `id`, `system_prompt`; `model` when not inherited; required `io.input.schema` and `io.output.schema`; `verify` / `gate_kinds` when the step needs them |
22
22
 
23
- Each pipeline stage entry has `id` and `uses: ./<id>.yaml` (path relative to the pipeline file). Non-root stages add `needs: <parent-id>` or `needs: [<parent-id>, …]` (length ≥ 1; strings or `{ id, on }`). A deciding stage adds `fork:`. Put `on_verify_fail` on the pipeline entry when after-phase `verify` should repair or wait for an operator. Filename stem matches `id` on every file. Optional pipeline-root `model` fills stages that omit `model`.
23
+ Each pipeline stage entry has `id` and `uses: ./<id>.yaml` (path relative to the pipeline file). At least one root marks `entry: true`. Sources list forward `route: [{ to: <id> }, …]`. Optional `on:` is skip-cascade policy (default succeeded-only); it does not launch from skipped/failed. Optional `if` gates a successor after the source succeeds (predicate on required `io.output.schema` fields). A review that can send work back adds `{ type: loop, to, max_replays, on_max_replays, replay_session }` inside `route`. A Clone Chain emitter puts `clone_cap` and `clone_mode` on that **pipeline entry**, never on the reusable stage body. Put `on_verify_fail` on the pipeline entry when after-phase `verify` should repair or wait for an operator. Filename stem matches `id`. Optional pipeline-root `model` fills stages that omit `model`. Sequencing rules: [`catalog-mapping.md`](catalog-mapping.md). Catalog load rejects `needs`, `fork`, and `feedback_loop` as YAML keys.
24
24
 
25
25
  ## Collisions
26
26