mlua-swarm-cli 0.12.1

Command line interface for mlua-swarm (mse binary with serve / mcp subcommands).
Documentation
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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# mse — Blueprint authoring guide

A **Blueprint** is the unified package of a `flow.ir` program plus the Swarm
extension layers (agent bindings, Operator role definitions, compiler
hints/strategy, metadata). This guide covers the shape you write by hand or
generate programmatically; for the exact, always-current JSON Schema fetch
`mse://api/blueprint-schema`.

## Top-level shape

```jsonc
{
  "schema_version": "0.1.0",          // optional, defaults to the current schema version
  "id": "my-blueprint",               // required, unique within your namespace
  "flow": { "kind": "seq", "children": [] }, // required, a flow.ir Node (see below)
  "agents": [ /* AgentDef[] */ ],      // optional, default []
  "operators": [ /* OperatorDef[] */ ], // optional, default []
  "hints": { "per_agent": {}, "global": {} }, // optional
  "strategy": { "strict_refs": true, "strict_kind": true }, // optional
  "metadata": { "description": "...", "tags": [] }, // optional
  "spawner_hints": { "layers": [] },   // optional, middleware capability keys
  "default_agent_kind": "operator",    // optional, defaults to "operator"
  "default_operator_kind": "automate", // optional, no default (falls through the cascade)
  "degradation_policy": "warn"         // optional, "warn" (default) | "fail" (opt-in, schema-only today — see the worker degradation reporting section in `mse://guides/operator-execution-model`)
}
```

All fields except `id` and `flow` are optional and fall back to sensible
defaults. `deny_unknown_fields` is enforced throughout the schema — a typo in
a field name is a hard parse error, not a silently-ignored key.

## Flow node kinds (`flow.ir` `Node`)

Every node is tagged with a `kind` discriminator:

| kind      | fields                                             | behavior                                                                                     |
|-----------|------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| `step`    | `ref`, `in`, `out`                                    | Dispatch the agent named `ref` with the evaluated `in` expr as input; write the result to `out` (must be a `path` expr). |
| `seq`     | `children` (`Node[]`)                                 | Evaluate children in order, threading the ctx through each.                                    |
| `branch`  | `cond`, `then`, `else`                                | Evaluate `cond` (must resolve to a JSON bool); run `then` if true, `else` if false.             |
| `loop`    | `counter`, `cond`, `body`, `max`                      | Writes `0` to `counter`, then repeats `body` while `cond` is truthy and `counter < max`, incrementing `counter` after each iteration. |
| `fanout`  | `items`, `bind`, `body`, `join`, `out`                | Evaluate `items` to an array; run `body` once per element (bound to `bind` in a branch-local ctx); aggregate into `out` per `join` mode: `all` (every branch runs, array of final ctx), `any` (first success wins), `race` (first to settle wins), `all_settled` (never raises, per-item `{status, value|reason}` record). |
| `try`     | `body`, `catch`, `err_at?`                            | Run `body`; on error, roll back ctx writes, optionally write the error message to `err_at`, then run `catch`. |
| `assign`  | `at`, `value`                                         | Pure ctx transform: evaluate `value` against the ctx snapshot and write it to `at`. No agent dispatch. |

`out` / `at` / `counter` must always be `path` exprs (write targets).

## Worker output: `out` vs named parts (GH #36)

A `step` node's OUTPUT is normally a single JSON value — the worker's
final `mse_worker_submit` `body` — addressable downstream via `{"op":
"path", "at": "$.<step>"}`.

A worker may additionally stage any number of *named* output parts
before completing the attempt: call `mse_worker_submit` with `name` set
(see `mse://guides/mcp-tool-reference` § Named multi-part output) once
per part, then finish with an ordinary plain (no-`name`) submit. The
set of `name` values a worker submits is its **staged-names allowlist**
— that allowlist alone determines what the engine folds into the
`parts` map on that step's OUTPUT. Any other artifact the worker (or a
middleware) emits — most notably the after-run audit sidecar
`audit:<step_ref>` (see § Reading prior-step OUTPUT below) — bypasses
the fold and is only reachable via the Worker axis. A step that staged
at least one part ends up with OUTPUT shape

```jsonc
{ "out": /* the final plain-submit body */ "...", "parts": { "plan.md": "...", "notes": { "todo": "..." } } }
```

instead of the plain final-submit body alone. A downstream step reads a
part with RFC 9535-style bracket-notation path syntax — required for any
key containing a literal `.`, like a filename:

```jsonc
{ "op": "path", "at": "$.<step>.parts[\"plan.md\"]" }
```

Bracket segments chain directly (`$.<step>.parts["a"]["b"]`) or combine
with dot segments in either order (`$.<step>.parts["notes"].todo`); keys
support no escaping (a literal `"` inside a name cannot be represented).

**Author caution**: once a step stages any parts, its OUTPUT becomes an
Object (`{"out": ..., "parts": {...}}`) instead of the plain final-submit
value — a downstream `eq`/`ne` expr comparing `$.<step>` directly against
a string (or other scalar) no longer matches; address `$.<step>.out`
instead (or a `parts[...]` entry). Keeping a worker's staging behavior in
sync with the Blueprint's `in` exprs that read its output is the
Blueprint author's responsibility — nothing in the schema enforces it
automatically.

## Reading prior-step OUTPUT (Worker axis): `context.steps`

The `$.<step>` / `$.<step>.parts[...]` paths above are the **BP axis** —
a downstream step declares what to read at Blueprint-authoring time and
the folded value flows into its `in`. Fine when the caller knows the
shape in advance.

The **Worker axis** is the complementary read-back path for a SubAgent
that decides at runtime which prior-step OUTPUT to pull. Every step's
OUTPUT is dual-recorded in the engine's `OutputStore` and surfaced via
`WorkerPayload.context.steps` as a `StepPointer` (GH #20 Contract C; see
`mse://guides/operator-execution-model` § Hop 4), filtered by the
current step's `ContextPolicy.steps` allowlist / `steps_exclude`
denylist:

```jsonc
"context": {
  "steps": {
    "planner":       { "name": "planner",       "size_bytes": 1834, "file_path": "/…/ctx/planner.json",       "content_url": "…", "sha256": "…" },
    "audit:planner": { "name": "audit:planner", "size_bytes":  412, "file_path": "/…/ctx/audit-planner.json", "content_url": "…", "sha256": "…" }
  }
}
```

Key facts:

- **Pointer-only invariant** — a `StepPointer` never carries the OUTPUT
  content itself (no preview, no content bytes inline). The SubAgent
  fetches the actual bytes via `file_path` (local FS `Read`) or
  `content_url` (server HTTP GET, verifiable against `sha256`). Choose
  `file_path` on same-host SubAgents (the common case); choose
  `content_url` when the SubAgent runs elsewhere.
- **`audit:<step_ref>` is a first-class entry** — after-run audit
  artifacts (GH #34) surface as top-level `context.steps` keys named
  `audit:<step_ref>`, **not** as a nested field of the audited step. The
  BP axis (`$.<step>.audit[...]` or similar) does not reach them — audit
  findings are observational sidecars that the fold-final path drops
  from the BP-chain value but the Data-plane dual-write preserves for
  Worker-axis consumers. See `mse://guides/operator-execution-model` §
  After-run audits.
- **Keys are canonical step names** — the map key is the step's
  canonical name resolved from `AgentMeta.projection_name` (GH #23), so
  `ContextPolicy.steps` / `steps_exclude` entries are matched against
  canonical names, not any renamed alias.
- **BP-chain scope vs Data-plane scope** — the fold-final path
  (`fold_final_and_parts`, `src/core/engine.rs`) only stages a step's
  `mse_worker_submit`-with-`name` artifacts (the `staged_names`
  allowlist from § Worker output) into the BP-chain value. Other
  artifacts (audits, out-of-band submissions) bypass fold but remain
  reachable via `context.steps`.

## Returning verdicts to drive BP flow (canonical pattern)

A **verdict** is a small scalar (e.g. `"PASS"`, `"BLOCKED"`, `"ALLOW"`) an
agent emits so that a downstream `branch` or `loop` node can compare it
via `eq($.<step>, lit("BLOCKED"))` and pick a path. `eq` is a
**structural** compare — the whole value at `$.<step>` must equal the
whole `lit(...)` value. That constraint decides how the agent shapes its
submit body.

Two shapes are canonical; a third is a frequently-attempted anti-pattern.

### Pattern A — plain body carries the verdict scalar

The agent's `mse_worker_submit` body is the verdict literal, nothing
else:

```
mse_worker_submit(body="BLOCKED")
```

- Step OUTPUT is exactly the string `"BLOCKED"` — no `parts` field.
- BP-side: a downstream `"in": {"op": "path", "at": "$.gate"}` observes
  the scalar directly, and
  `{"op": "eq", "lhs": {"op": "path", "at": "$.gate"}, "rhs": {"op": "lit", "value": "BLOCKED"}}`
  matches.
- Trade-off: the submit body has room for the verdict only — no
  human-readable report co-exists with it on that step.
- When to use: pure gates where the verdict is the whole point. Working
  example: `mse://blueprints/samples/03-fn-override` — the `mock-gate`
  agent's system prompt is literally `` Always reply `BLOCKED` ``, and
  the top-level `branch` fires on `eq($.verdict, lit("BLOCKED"))`.

### Pattern B — named part carries the verdict, plain body carries the report

The agent stages the verdict as a named part first, then finishes the
attempt with a plain (unnamed) submit whose body is the human-readable
report:

```
mse_worker_submit(name="verdict", body="BLOCKED")     # stage the verdict
mse_worker_submit(body=<full YAML / markdown report>) # finish the attempt
```

- Step OUTPUT shape becomes
  `{"out": <the full report>, "parts": {"verdict": "BLOCKED"}}`.
- BP-side: the verdict is addressed with bracket notation —
  `{"op": "eq", "lhs": {"op": "path", "at": "$.gate.parts[\"verdict\"]"}, "rhs": {"op": "lit", "value": "BLOCKED"}}`
  — while the full report stays reachable as `$.gate.out` for
  downstream consumers (a resolver agent that needs the failure detail,
  or a report artifact for humans).
- Trade-off: the agent issues two `mse_worker_submit` calls, and the BP
  author must know to address the `parts["verdict"]` entry, not the
  plain step name.
- When to use: gates whose verdict must drive flow *and* whose full
  report is a first-class artifact.

### Anti-pattern — full report in the plain body, `eq` against the step name

An agent that submits a full YAML/markdown report as its only submit
body and expects `eq($.gate, lit("BLOCKED"))` to fire **cannot work**:
`$.gate` resolves to the whole report string; `lit("BLOCKED")` is the
four-character literal; they never compare equal. The `branch`'s `else`
path fires on every dispatch, which reads to a caller like the verdict
path was silently swallowed even though the agent said `BLOCKED`.

Debug rule: if a gate's `then` path never fires while the agent visibly
outputs a verdict word, check the submit shape first. It must be a
scalar (Pattern A) or a named part (Pattern B) — `$.gate` cannot be a
report body that *contains* the verdict word.

### Enforcing verdict contracts (opt-in)

Pattern A/B above are conventions — until an agent opts in, nothing
checks that its submit shape actually matches how a downstream `cond`
addresses it. `AgentDef.verdict` is an **optional** field that turns
that convention into two machine checks. It is strictly additive: an
agent that declares no `verdict` behaves exactly as before, byte for
byte, at both boundaries described below. The working sample
`mse://blueprints/samples/02-verdict-loop` is a live example of this —
its `mock-gate` agent declares no `verdict` field and continues to
register and run unchanged; Pattern A's convention alone is still
enough for it.

Declare a contract on the agent whose output a `cond` will compare:

```jsonc
// channel: "body" — Pattern A, the plain step OUTPUT IS the verdict
"agents": [{
  "name": "gate",
  "verdict": {
    "channel": "body",
    "values": ["PASS", "BLOCKED"]
  }
}]
```

```jsonc
// channel: "part" — Pattern B, the verdict is staged as the named part
"agents": [{
  "name": "gate",
  "verdict": {
    "channel": "part",
    "values": ["PASS", "BLOCKED"]
  }
}]
```

`channel: "part"` addresses one literal part name only —
`mse_worker_submit(name="verdict", body=...)` / `$.gate.parts["verdict"]`
— the way Pattern B is documented above. `values` is a closed set of
tokens; a comparison against anything outside it is a violation.

**Register time (compile, read-only lint).** `Compiler::compile` walks
every `Branch`/`Loop` `cond`'s `Eq`/`Ne`/`In` comparisons of a step
output `Path` against a literal, resolves the `Path` back to its
producing agent, and — only for agents that declared a `verdict` —
checks two things:

- The `Path` addresses the channel the agent declared (bare `$.<step>`
  for `channel: "body"`, `$.<step>.parts.verdict` /
  `$.<step>.parts["verdict"]` for `channel: "part"`). A mismatch fails
  the compile with `CompileError::VerdictChannelMismatch`, naming the
  step, the declared channel, and the channel the `cond` actually
  addressed.
- Every literal compared against that `Path` (including every entry of
  an `In` haystack) is a member of the declared `values`. A literal
  outside the set fails the compile with
  `CompileError::VerdictValueNotInContract`, naming the offending
  literal and the declared set.

Compile fails on the **first** violation found (same posture as the
compiler's other static checks). If the `cond` references an agent
that declared **no** `verdict` field, nothing is rejected — at most a
`tracing::warn!` is emitted, and compilation still succeeds. This is
what keeps every pre-existing Blueprint, and every Blueprint whose
authors haven't opted in yet, compiling unchanged.

**Completion time (server, fail-loud producer gate — all 3 completion
routes).** A contract-bearing agent's attempt can complete through 3
different routes: `POST /v1/worker/submit`, the older
`POST /v1/worker/result`, or the WS Operator fallback (a worker
process that never POSTs at all). GH #50 originally gated only the
first of these, and only for `channel: "body"`; GH #51 closes the
remaining gaps by moving the check to the single choke point every
route funnels through — `Engine::submit_worker_result_trusted` /
`Engine::submit_output`, embedded immediately before the value is
written to `output_tail`, not re-implemented per route handler:

- `channel: "body"` — the completing value must be a member of
  `values`.
- `channel: "part"` — a staged `"verdict"` artifact must exist for the
  attempt (**presence**, not just membership — a worker that never
  calls `POST /v1/worker/artifact?name=verdict` at all is now rejected,
  the gap GH #51 exists to close) AND its value must be a member of
  `values`.
- `ok=false` completions are exempt on every route, identically — a
  transport-level failure (`DispatchOutcome::Blocked`, the flow.ir Try
  path) is not a verdict and is never validated against the contract.
- An agent that declared no contract, or declared a contract for the
  other channel, sees this gate as a no-op — behavior unchanged from
  before GH #50/#51.

A violation is rejected **before** the value reaches `output_tail` / the
flow ctx: `POST /v1/worker/submit` and `POST /v1/worker/result` both
surface HTTP 422, echoing the declared `values` (`channel: "body"`
violations) or naming the missing `"verdict"` part (`channel: "part"`
violations). The WS Operator route has no HTTP response to return a 422
on — a rejected completion there simply never writes its `Final`; the
attempt's `output_tail` has no `Final` and the downstream dispatch path
naturally treats it as incomplete (a `tracing::warn!` is logged
server-side). No new WS protocol message is introduced for this — the
deliberate "zero flow-ir changes" design choice, not a gap left to
fill.

The staging-time check at `POST /v1/worker/artifact?name=verdict`
(`channel: "part"` membership only, not presence) still runs — it gives
the worker the fastest possible feedback the moment it stages a bad
token. The completion-time check above is the backstop that guarantees
enforcement no matter which of the 3 routes an agent's attempt actually
completes through.

Together, the register-time and completion-time boundaries turn both
halves of the silent never-match anti-pattern above into loud
failures: an authoring mistake (`cond` addressing the wrong channel, or
comparing against a token the agent will never emit) stops at register
time; a worker that emits a full report where a token was expected, or
skips staging the verdict part entirely, stops at completion time on
every route it could have completed through. Neither boundary touches
`flow.ir` itself — no new `Expr` forms, no eval hooks, no `FlowNode`
rewriting; `Blueprint.flow` stays exactly what the author wrote, and
the contract lives entirely in the Blueprint/schema/compiler/server
layers described here.

### Declared verdict values must be handled downstream (opt-in strict mode)

The two register-time checks above are the **forward-direction** lint:
"every `Lit` a `cond` compares against must be a member of the agent's
declared `verdict.values`" and "every `cond` must address the declared
channel." The **reverse-direction** lint — "every entry of the declared
`verdict.values` set must be referenced by at least one downstream
`Branch`/`Loop` `cond`" — catches the complementary drift where a flow
author declares a verdict value (e.g. `"BLOCKED"`) but forgets to write
a branch that handles it.

By default the reverse-direction lint only surfaces
`tracing::warn!` — the compile still succeeds. This preserves back-
compat with existing Blueprints that intentionally leave some declared
values as silent-pass informational tokens (an agent may want to
document "we may emit `INFO` as well" without demanding every caller
branch on it).

To promote the warning to a hard `CompileError::VerdictValueUnhandled`,
opt in via `Blueprint.metadata`:

```json
{
  "metadata": {
    "strict_verdict_handling": true
  }
}
```

Under `strict_verdict_handling: true`, `Compiler::compile` rejects any
Blueprint where a contract-bearing agent declares a `verdict.values`
entry that no downstream `Branch`/`Loop` `cond` references. The
diagnostic names the agent, the unhandled value, the full declared
`values` set (so the fix is unambiguous — either add a handler branch
or drop the value from the declaration), and the `Step.ref_` where the
agent is invoked (best-effort — when the agent is invoked at multiple
sites, the first-encountered site is reported).

Two ways to satisfy the strict lint:

1. **Add a branch per declared value.** The canonical shape — one
   `Branch` per value, or one `Branch` per value pair (e.g. `"PASS"` in
   the `then_`, `"BLOCKED"` in the `else_`).
2. **Cover the whole set with one `In`.** An `In` cond whose `Lit`
   haystack lists every declared value counts every entry as handled in
   one node — useful when the flow author wants a single "any of these
   verdict values ⇒ proceed" branch.

The forward and reverse lints run in the same walk, so the strict
setting has no extra runtime cost; either both fire or neither does.
The setting is a Blueprint-level opt-in (per BP, not per agent), so a
flow author who wants the strict check on some agents but not others
can either split those agents into a separate Blueprint or leave the
setting off and rely on the default `tracing::warn!` output.

### Cross-links

- Named-parts wire format and OUTPUT shape: § Worker output: `out` vs
  named parts (above).
- Working samples that exercise Pattern A end-to-end:
  `mse://blueprints/samples/02-verdict-loop` (a `loop` that retries
  while `$.verdict == "BLOCKED"`) and
  `mse://blueprints/samples/03-fn-override` (a `branch` that hands a
  BLOCKED gate result to an approver step).
- Agent-side declaration (which pattern the agent's own Output format
  section commits to): `mse://guides/agent-md-authoring` § Output
  contract: inline body vs `@file:` sentinel.
- The static verifier that surfaces some verdict-related drift
  (`declared_tools` vs wrapper grants, projection-name/parts-shape
  changes downstream): `mse://guides/agent-md-authoring` § Verifying
  how your agent materializes.

## Expr ops (`flow.ir` `Expr`)

Every expr is tagged with an `op` discriminator:

| op       | fields                    | result                                                                 |
|----------|---------------------------|-------------------------------------------------------------------------|
| `path`   | `at` (e.g. `"$.x.y"`)      | Read a value from ctx. Raises if the path is missing.                   |
| `lit`    | `value`                    | A literal JSON value.                                                   |
| `eq`     | `lhs`, `rhs`               | Structural equality.                                                     |
| `ne`     | `lhs`, `rhs`               | Structural inequality.                                                   |
| `lt` / `lte` / `gt` / `gte` | `lhs`, `rhs` | Comparison: both numbers (`f64`) or both strings (lexicographic, Lua `<` parity). Mixed types raise. |
| `not`    | `arg`                      | Boolean negation (truthy-based).                                        |
| `and`    | `args` (array)             | Short-circuit conjunction; empty array → `true`.                        |
| `or`     | `args` (array)             | Short-circuit disjunction; empty array → `false`.                       |
| `exists` | `arg` (expr)               | `true` iff `arg` resolves to a non-`null` value (missing path → `false`, present-but-`null``false`). |
| `add` / `sub` / `mul` / `div` / `mod` | `lhs`, `rhs` | Numeric arithmetic (`f64`); `div` / `mod` by zero raises. `mod` follows Lua `%` (result takes the sign of `rhs`). |
| `len`    | `arg`                      | Element count (array), char count (string), or key count (object).      |
| `in`     | `needle`, `haystack`       | `true` if `needle` equals any element of the `haystack` array.          |
| `call_extern` | `ref`, `args` (array) | Invoke a host-registered pure function (`Externs` registry) with the evaluated `args`. Unregistered `ref` raises. Value-shape only — no side effects, no flow control. |

`call_extern` requires the host to register an externs registry
(`TaskLaunchService::with_externs`); without one every `call_extern`
raises an extern error.

Truthy semantics match Lua/JS: `null`/`false` are falsy, everything else
(including `0` and `""`) is truthy.

## Agents (`AgentDef`) and kind resolution

### Two authoring paths

An `AgentDef` can be written in two places, and either is fine:

- **Direct JSON literal (this guide's default form)** — the
  `AgentDef` object appears inline inside the Blueprint JSON. All
  fields (`name`, `kind`, `spec`, `profile.system_prompt`,
  `profile.worker_binding`, `profile.tools`, `meta`, …) are set
  literally in the JSON tree. This is the default authoring shape
  for the samples under `mse://blueprints/samples/*` and for
  programmatic authoring (algocline strategies, skills, dogfood
  harnesses).
- **`$agent_md` file ref** — the entry is a single-key object
  `{ "$agent_md": "agents/foo.md" }` and the loader parses the
  target file's frontmatter (+ Markdown body) into a
  fully-populated `AgentDef`. See the `$agent_md file-ref
  expansion` section below.

Compile-time error messages that name a field (e.g.
`profile.worker_binding`) are actionable on either path — for JSON
authors, add the field to the JSON literal; for `$agent_md` authors,
add it to the `.md` frontmatter. The messages themselves spell both
paths out.

### `AgentDef` shape (JSON-direct form)

Each entry in `agents` maps a name (referenced from `flow.Step.ref`) to a
backend:

```jsonc
{
  "name": "my-agent",
  "kind": "rust_fn",           // lua | rust_fn | agent_block | subprocess | operator
  "spec": { "fn_id": "..." },  // free-form, interpreted per kind
  "profile": { "system_prompt": "...", "model": "...", "tools": [] }, // optional
  "meta": { "description": "...", "tags": [] } // optional
}
```

`AgentKind` is a closed enum (`lua`, `rust_fn`, `agent_block`, `subprocess`,
`operator`) — there is no string-escape-hatch variant. When an agent omits
`kind`, resolution falls through a four-tier cascade (highest to lowest
priority): (1) per-`AgentDef.kind` literal, (2) the Blueprint's top-level
`default_agent_kind`, (3) a CLI-level default (e.g. `mse serve
--default-agent-kind`), (4) the schema `Default` impl (`operator`).

### `$agent_md` file-ref expansion

Instead of writing an `AgentDef` object inline, you can reference an
`agent.md` file (frontmatter + Markdown body) and let the loader expand it:

```jsonc
{ "agents": [ { "$agent_md": "agents/domain-researcher.md" } ] }
```

This parses the file's frontmatter + body into a fully-populated `AgentDef`
(`profile.system_prompt`, `meta`, `spec`, etc.). Sibling keys alongside
`$agent_md` are shallow-merged onto the expanded object afterward — handy for
overriding just `spec.operator_ref` or `meta` while keeping the rest of the
`agent.md` content:

```jsonc
{ "$agent_md": "agents/domain-researcher.md", "spec": { "operator_ref": "role-a" } }
```

**Path hygiene**: refs are resolved relative to the Blueprint file's own
directory. Absolute paths and any `..` parent-directory component are
rejected — refs are sandboxed inside the Blueprint's base-directory subtree.
The same rule applies to the more general `$file` ref (`{"$file": "path"}`),
which substitutes a referenced file's raw string contents anywhere in the
JSON tree (e.g. externalizing a large prompt out of a `Step.in` literal).

### Runners (GH #46): `Blueprint.runners` / `AgentDef.runner` / `runner_ref`

A `Runner` declares the execution shell an agent's Worker IMPL dispatches
into — tool grant, model selection, and runtime capabilities for the
backend it targets. Two variants exist today: `ws_claude_code` (Claude
Code subagent wrapper; `variant` = the wrapper's `subagent_type`, `tools`
mirrors the wrapper frontmatter) and `agent_block_in_process`
(agent-block in-process runtime; `tools` is the effective, enforced tool
set). `AgentDef.kind = agent_block` pairs with an `agent_block_in_process`
Runner; every other `AgentDef.kind` pairs with `ws_claude_code`.

Runners are declared through a named, BP-level registry
(`Blueprint.runners: [{ "name": ..., "runner": {...} }]`) — the same
registry shape as `Blueprint.metas` — and resolved per-agent through a
5-tier cascade (highest priority first):

1. `AgentDef.runner` — an inline `Runner` object on the agent itself.
2. `AgentDef.runner_ref` — a name looked up in `Blueprint.runners`.
3. Legacy fallback: `profile.worker_binding` synthesizes a
   `ws_claude_code` Runner from `{ variant: worker_binding, tools:
   profile.tools }`**deprecated, kept for one release cycle** while
   Blueprints migrate onto `runner` / `runner_ref`.
4. `Blueprint.default_runner` — a BP-wide registry name, used only when
   no tier above (1–3) applies to this agent.
5. No Runner declared through any tier — the agent has none.

Note tier 3 outranks tier 4: an agent's own `profile.worker_binding`
still wins over the Blueprint's `default_runner`, mirroring the
`AgentInline > MetaRef > BpGlobal` precedence the ctx-supply cascade
already follows (agent-level declarations always beat BP-global ones).

```jsonc
{
  "runners": [
    { "name": "claude-worker", "runner": {
        "backend": "ws_claude_code", "variant": "mse-worker-coder", "tools": ["Read", "Edit"]
    } }
  ],
  "default_runner": "claude-worker",
  "agents": [
    { "name": "coder", "kind": "operator", "spec": { "operator_ref": "role-a" }, "runner_ref": "claude-worker" }
  ]
}
```

This Milestone (M2) only adds the schema, registry, and resolver — wiring
the resolved Runner into the launch path is a later Milestone; today's
launch path still reads `profile.worker_binding` directly.

## Versioning

`metadata.version_label` is an optional free-form SemVer string (e.g.
`"1.2.3"`) used as the match target when reading a stored Blueprint by
version. Store readers select a version via one of three selectors:

- `Latest` — the store's current head (the default when unspecified).
- `Fixed { value }` — one exact, previously-committed version.
- `SemverReq { req }` — resolve to the newest stored version whose
  `version_label` satisfies a `semver::VersionReq` (e.g. `"^1.2"`).

`version_label` is rewritten automatically by the Enhance loop on
PATCH/MINOR/MAJOR bumps; you do not need to hand-maintain it once a
Blueprint is under Enhance management.

## Where to go next

- Three worked examples: `mse://blueprints/samples/01-pure-ctx-eval` (zero
  agent dispatch, pure ctx math), `mse://blueprints/samples/02-verdict-loop`
  (retry loop with a self-managed counter), `mse://blueprints/samples/03-fn-override`
  (a blocked verdict overridden by an approver step).
- The exact, always-current JSON Schema: `mse://api/blueprint-schema` (note:
  `flow` itself is opaque in the schema — its grammar is owned by the
  `mlua-flow-ir` crate, referenced above).
- Tool-level operations (running, archiving, schema fetch): `mse://guides/mcp-tool-reference`.
- Verifying an `AgentDef`'s materialized tools/ctx/output before a run
  (`bp_explain_agent`): `mse://guides/agent-md-authoring` §
  Verifying how your agent materializes.
- The DSL surface for authoring Blueprints directly in Lua (Expr method chains, Node builders, bp_dsl pipeline sugar): mse://guides/dsl-authoring, with two DSL samples: mse://blueprints/samples/06-dsl-verdict-loop and mse://blueprints/samples/07-dsl-pipeline.