1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
--- bp_dsl.lua — pure-Lua internal DSL for the Blueprint vocabulary.
---
--- `B = require("bp_dsl")`. Depends on `flow_dsl` (`F`) for Expr / Node
--- construction; `flow_dsl` does NOT depend on `bp_dsl` (one-way
--- dependency).
---
--- `B.pipeline{}` is the authoring sugar this module exists for: default
--- `in` / `out` wiring, automatic per-stage verdict-gate insertion, and a
--- 3-part retry-loop expansion. `B.stage` is a curried 2-arg constructor
--- (`B.stage "id" { ... }`) that returns a plain "stage record" — NOT yet
--- an AST Node; `B.pipeline` is what turns stage records into flow.ir
--- Nodes.
local F = require
local M =
--- `B.bp{ id=, agents=, flow=, ... }` — the whole Blueprint table.
--- bp_dsl does not gate-keep field names: anything beyond `id` / `flow`
--- (e.g. `agents`, `operators`, `strategy`, `metadata`) passes through
--- verbatim — the Blueprint schema itself is the source of truth for
--- what's valid there.
--- `B.agent{ md=, verdict=, ... }` — an `$agent_md` file-ref `AgentDef`
--- entry: `{ ["$agent_md"] = md, verdict = verdict, ... }`. Every sibling
--- field besides `md` passes through verbatim, mirroring the loader's own
--- shallow-merge-onto-`$agent_md` semantics (see the guide's `$agent_md`
--- file-ref expansion section).
-- ── Stage records + B.from placeholders ──────────────────────────────────
local Placeholder =
Placeholder. = Placeholder
local
--- `B.from "stage_id"` — an unresolved reference to another stage's `out`
--- path. Resolved by `B.pipeline` once every stage's `out` is known (so
--- forward references — a stage referencing one declared later in the
--- same pipeline — work too); referencing an undefined stage id is an
--- `error()` at `B.pipeline` time.
--- `B.stage "id" { agent=, input=, out=, gate=, halt_on=, retry= }` —
--- curried 2-arg stage constructor. Returns a plain "stage record" table
--- (NOT an AST Node yet — `B.pipeline` expands stage records into
--- flow.ir Nodes, applying the default-wiring + gate + retry rules
--- below). `halt_on` here overrides `B.pipeline`'s pipeline-wide default
--- for this stage only.
-- ── B.pipeline: default wiring + gate + retry expansion ─────────────────
-- A stage's default `in` path (used when the stage record's own `input`
-- is nil and it isn't a `B.from` placeholder either).
local
-- A stage's default `out` path.
local
-- A stage's default retry-loop counter path (used when the stage's own
-- `retry.counter` is nil).
local
-- The verdict-gate condition for one stage: `eq(<out>.parts["verdict"],
-- lit(v))` for a single halt_on value, or an N-ary `or` of that shape
-- across every halt_on value when there's more than one.
local
-- Resolve a stage's `input` field to a path string: `B.from "x"`
-- placeholders resolve against `outs` (populated for every stage, and
-- every retry `fix` stage, before any Node is built — so both forward and
-- backward references work); `nil` falls back to `chain_default` when the
-- caller supplies one (see `B.pipeline`'s `chain` option), otherwise to
-- the R1 default; any other value is assumed to already be a path string.
local
-- Build the `step` Node for one stage record. `outs` must already carry
-- this stage's own resolved `out` path (`rec._out`, set by the
-- register-outs pass below) so R6 `B.from` references to THIS stage
-- resolve correctly even from earlier stages in the list. `chain_default`
-- (nilable) is the `input` fallback for a stage whose `input` is `nil` —
-- see `B.pipeline`'s `chain` option.
local
--- `B.pipeline{ stage..., halt_on={"BLOCKED"}, halted_at="$.halted_at",
--- done="$.xxx" }` — the default-wiring authoring sugar. Positional
--- entries are stage records (`B.stage "id" {...}`); `halt_on` /
--- `halted_at` / `done` are pipeline-wide options. Returns a `seq` Node
--- (a raw flow.ir table).
---
--- ## Default in/out
---
--- A stage's `input` defaults to `$.d.{stage_id}`; its `out` defaults to
--- `$.{stage_id}`. An explicit `input` / `out` on the stage record
--- overrides the default (per-stage `input` may also be a `B.from`
--- placeholder — see R6 below).
---
--- ## Chained pipelines
---
--- `chain = true` at the top level of the pipeline spec changes the
--- `input` fallback for stage N (N ≥ 2) from `$.d.{stage_id}` to
--- `$.{stage[N-1]_id}` — i.e. each stage reads the previous stage's own
--- `out` path. Stage 1's default is unchanged (still `$.d.{stage_1_id}`).
--- An explicit `input` on a stage record (whether a path string or a
--- `B.from` placeholder) still overrides the chained default. Retry
--- `fix` stages are not chained: they retain the R1 default so the
--- fixer's own input can be seeded independently of the review stage's
--- output. Omitting `chain` (or setting it `false`) preserves the R1
--- default in every position.
---
--- ## Opt-in verdict gate (bafe47d4)
---
--- A stage emits a verdict gate iff it opts in explicitly. The default
--- is NO gate (fix for the pre-bafe47d4 dead-branch pattern where every
--- stage in a pipeline with pipeline-level `halt_on` got a gate whose
--- `cond` compared against a verdict the stage never emitted).
---
--- Opt-in rules (any one triggers gate emission):
--- - `gate = true` explicit on the stage record.
--- - `halt_on = {...}` set on the stage record (declares halt values,
--- implies the stage means to gate).
--- - `retry = {...}` set on the stage record (the retry loop reads
--- verdict; the post-retry gate makes sense).
--- - `gate_default = "auto"` at the pipeline level restores the old
--- cascade — pipeline-level `halt_on` is inherited by every stage
--- whose own `gate` / `halt_on` / `retry` are all unset, and they
--- emit a gate. This is an escape hatch for pre-fix bp.lua sources
--- that want their existing shape preserved; new code should not
--- use it. Default is `gate_default = "explicit"` (the new,
--- bug-fixed behavior).
---
--- `gate = false` overrides all four (opts out even with retry / stage
--- halt_on / auto cascade). When set the stage's step spliced directly
--- into the enclosing `seq` with no `branch`, and the rest of the
--- pipeline continues unconditionally (NOT nested under an `else`).
---
--- When a gate emits, its shape:
--- - `cond`: `eq(path(<out>.parts["verdict"]), lit(halt_on_value))`
--- — `or`-combined across every value when there is more than one.
--- Stage-level `halt_on` supersedes pipeline-level for the cond
--- value list; the pipeline-level list stays a shared default for
--- opted-in stages that do not name their own values.
--- - `then` (halt): `assign{at=halted_at, value=lit(stage_id)}`.
--- Every remaining stage is skipped.
--- - `else`: the rest of the pipeline (next stage's step + its own
--- gate if any). The innermost `else` (after the last stage) is
--- `assign{at=done, value=lit(true)}` when `done` was given, or an
--- empty `seq{}` otherwise.
---
--- ## Retry
---
--- `retry = { max = N, fix = <stage record>, counter = "$.path" }` on a
--- stage record expands to 3 parts, in order: (1) the stage's own `step`
--- Node; (2) `loop_{counter = <counter path>, cond = <lt(counter, max)
--- AND <the gate cond above>>, max = max + 1, body = seq{fix step, stage
--- step re-run}}`; (3) the ordinary verdict gate (evaluated once more,
--- after the loop settles — or spliced in directly if `gate = false`).
--- `counter` is optional; when omitted the loop counter path defaults to
--- `"$.{stage_id}_n"`. The `fix` stage record goes through the same
--- default in/out wiring as any other stage.
---
--- ## `B.from`
---
--- Resolved against every stage's `out` path (including retry `fix`
--- stages) before any Node is assembled, so forward references work; an
--- unresolved reference is an `error()`.
return M