Joins prompt elements from a library into a compound output. The JOIN operator. Has three modes selected by flag combinations.
v0.2 migration note. This page documents the v0.2 schema. The
orderinteger is gone; compound outputs are taggedkind = "compound"and carry adepthfield next to the existingbody_level. If you have v0.1 files on disk, runoovra migrate <library-dir>to convert them in place.
oovra compose <ID>... [common flags] # File mode: write a new composed .md
oovra compose <ID>... --text [common flags] # Text mode: print clean prose to stdout
oovra compose --re-render <PATH> [common flags] # Re-render an existing compound file's body
Common flags across all modes:
| Flag | Default | Purpose |
|---|---|---|
--library <DIR> |
./elements |
Directory to resolve input IDs from (recursive walk) |
--out-id <ID> |
composed-<IDs-joined> |
ID for the produced compound file |
--out-name <NAME> |
the value of --out-id |
Human-readable name for the produced file |
--out-version <V> |
"1.0.0" |
Semver string for the produced file |
--out-meta <META> |
"" |
Free-form description |
oovra compose <ID-1> <ID-2> ... <ID-N> --output <PATH> [common flags]
Resolves each <ID> against the library, computes the output's body_level and depth, renders the body (embedding each input's full source file wrapped in chiral delimiters), and writes a complete Oovra file to --output <PATH>.
| Flag | Required? | Default | Purpose |
|---|---|---|---|
<ID>... positional |
yes (≥1) | — | Input element IDs in the sequence they should appear in the composition. The sequence is preserved in composed_of and the body. |
-o, --output <PATH> |
no | <library>/<out-id>.md |
Where to write the compound file. Defaults to your library directory + the out-id + .md. |
A complete Oovra file at the output path. Its frontmatter has:
name←--out-name(or--out-idif not given)kind←"compound"(always — Mode 1 only produces compounds)id←--out-id(or auto-generated)version←--out-version(default"1.0.0")meta←--out-meta(default"")generated_at← current UTC time as RFC 3339 (set by the tool, never user-provided)render_mode←"markdown-h2"(v0.2 has one renderer)body_level← computed bycompute_body_level(input_body_levels) = max(input.body_level, default = 0) + 1depth← computed bycompute_depth(input_depths) = max(input.depth, default = 0) + 1(numerically equal tobody_levelfor every valid compound; see kind-and-delimiters.md)composed_of← array of{id, version}for each input, in argument order, with versions from the library
The body is the concatenation of each input's full file content (frontmatter + body), each wrapped between ~~...~~>> and ~~...~~<< lines whose tilde count is body_level + 1. See kind-and-delimiters.md for the full spec.
oovra compose --library ./elements \
--out-id coding-agent --out-name "Coding Agent" --out-meta "Strict 3-element pair" \
-o ./elements/coding-agent.md \
role-statement safety-fence tone-directOutput (header excerpt):
+++
name = "Coding Agent"
kind = "compound"
id = "coding-agent"
version = "1.0.0"
meta = "Strict 3-element pair"
generated_at = "2026-05-10T18:56:15.993212217+00:00"
render_mode = "markdown-h2"
body_level = 1
depth = 1
[[composed_of]]
id = "role-statement"
version = "1.0.0"
[[composed_of]]
id = "safety-fence"
version = "1.0.0"
[[composed_of]]
id = "tone-direct"
version = "1.0.0"
+++Followed by a body containing three ~~>> ... ~~<< chunks — one per input, each containing that input's full file content.
Inputs are emitted in the body in the exact sequence of the positional arguments. The composed_of array preserves this sequence. This affects:
- The sequence H2 sections appear in
compose --textoutput - The sequence leaves appear in
decompose --fulloutput (when listed) compare's structural diff (see notes there about set vs sequence semantics)
If you want a different rendering sequence, re-run Compose with a different argument order.
| Trigger | Error |
|---|---|
Zero positional input IDs (without --re-render) |
EmptyCompose |
<ID> not found in library |
ElementNotFound |
| Library directory has duplicate IDs | DuplicateId (raised by Library::load before compose runs) |
Output --out-id is not kebab-case |
InvalidField { field: "id" } — file is not written |
--output path is in a directory that doesn't exist |
The directory is auto-created; no error in normal cases. WriteIo if creation fails (permissions). |
oovra compose <ID-1> <ID-2> ... <ID-N> --text [common flags]
Resolves the inputs, recursively flattens through every level of composition, and prints clean Markdown to stdout. Nothing is written to disk. The output is intentionally not a valid Oovra file — there's no frontmatter, no delimiters, just ## <id> headers and the bodies.
For each atom in the tree, one section:
## <id-of-leaf-1>
<body of leaf 1>
## <id-of-leaf-2>
<body of leaf 2>
...Compound inputs are recursed through: their own ID never appears in the output, only the IDs of the atom leaves they ultimately contain. A depth = 3 input flattens to whatever atom leaves are inside it, in composition order.
This mode is the intended way to get a paste-ready system prompt out of Oovra. It is lossy — version numbers, metadata, generation timestamps don't appear — but that's intentional. A model doesn't need that information; you do, and you can recover it from the on-disk form.
Library has role-statement (atom), safety-fence (atom), tone-direct (atom).
oovra compose --library ./elements --text role-statement safety-fence tone-directOutput to stdout:
## role-statement
You are a senior software engineer pairing with the developer at the keyboard. Stay grounded in the actual code at hand.
## safety-fence
When asked to produce code that would damage a system, decline once briefly and offer the closest legitimate alternative.
## tone-direct
Be direct. Skip preamble. Skip apology. State conclusions before reasoning.If one of the input IDs is itself a compound element, the recursion descends into it. For example, if coding-agent is a compound (body_level 1) composed of three atoms, then:
oovra compose --library ./elements --text coding-agentproduces the same three H2 sections as if you had typed the three atom IDs directly. The compound wrapper is invisible in the prose form. See demos/v0.1/03-deep-text-flattening for a body_level-2 example.
| Trigger | Error |
|---|---|
| Zero positional IDs | clap error (positional required) |
<ID> not in library |
anyhow!("element '{id}' not found in library") |
| Sub-element decompose fails | BodyParse (rare; would indicate a corrupted compound file in the library) |
--text cannot conflict with --output (clap-level conflict). Producing a file and printing prose are different operations — choose one.
oovra compose --re-render <PATH> [common flags except positional IDs]
Reads the existing compound file at <PATH>, extracts its composed_of recipe, re-resolves every entry against the current library state with strict version-pin enforcement, and overwrites the file's body with a freshly rendered version.
You've changed something about a renderer or moved code around and want the body bytes regenerated without changing the recipe or the version pins. Useful for:
- Refreshing
generated_atto track when the file was last rebuilt - Catching cases where the body has been hand-edited (the re-render will diverge from the hand-edit, surfacing the drift)
- Future-proofing for when a future renderer ships — re-render the whole library against the new renderer
Unlike Mode 1 (which picks up whatever version is in the library), Mode 3 enforces that each input's library version exactly matches the version pinned in the file's existing composed_of. If any input's version has changed in the library, re-render fails with VersionMismatch.
This is intentional: if you wanted to absorb new versions, you'd run Mode 1 with the same input IDs (no pin enforcement). Re-render is for "regenerate against the SAME inputs at the SAME versions" — drift detection, not drift absorption.
Existing ./elements/coding-agent.md has composed_of pinning role-statement = 1.0.0, safety-fence = 1.0.0, tone-direct = 1.0.0.
If the library still has those versions:
oovra compose --library ./elements --re-render ./elements/coding-agent.md
# → Re-rendered ./elements/coding-agent.md (body_level 1)If role-statement has been bumped to 1.1.0 in the library:
oovra compose --library ./elements --re-render ./elements/coding-agent.md
# → Error: Version mismatch for 'role-statement': pin '1.0.0' does not match library version '1.1.0'You'd then either revert the version, or re-run Mode 1 to produce a new composition with the new version:
oovra compose --library ./elements --out-id coding-agent --out-version 1.1.0 \
-o ./elements/coding-agent.md \
role-statement safety-fence tone-direct| Trigger | Error |
|---|---|
<PATH> does not exist |
FileNotFound |
<PATH> is an atom (no composed_of) |
anyhow!("--re-render target is an atom and has no composed_of") |
Library is missing an input named in composed_of |
ElementNotFound |
| Library version of an input differs from its pin | VersionMismatch |
When Compose produces a file, it computes two numbers from the inputs. Both use the same strict-escalation rule — the v0.1 count-based compute_order formula is gone.
body_level (the physical on-disk delimiter level):
output.body_level = max(input.body_level, default = 0) + 1
Atoms contribute body_level = 0 (they have no body_level field on disk; the formula treats absence as 0).
depth (the human-friendly compositional-depth label, mirrors body_level):
output.depth = max(input.depth, default = 0) + 1
The two are numerically equal for every valid compound; depth is exposed as a separate field so downstream tooling can read "how deep is this recipe tree" without knowing the delimiter-level convention. See kind-and-delimiters.md.
| Inputs | body_level |
depth |
|---|---|---|
| 3 × atom | 1 | 1 |
| 2 × compound at body_level 1 | 2 | 2 |
| 1 × compound at body_level 1 + 2 × atom | 2 | 2 |
| 1 × compound at body_level 2 + 1 × compound at body_level 1 + 4 × atom | 3 | 3 |
- schema.md — the file format Compose produces
- kind-and-delimiters.md — the formulas and the delimiter spec
- command-decompose.md — the inverse operation; reads what Compose produces
- errors.md — every error Compose can surface
- demos/v0.1/03-deep-text-flattening —
--texton a body_level-2 compound - demos/v0.1/05-mixed-order-regression — the mixed-input case the strict-escalation rule was designed for
- demos/v0.2/01-operator-gamut — full Compose→Decompose→Compare run with two library snapshots