Directive composition and fan-out¶
This page documents directives for review, follow-up, repetition, and prompt fan-out.
Editor Review Marker (@)¶
The old %edit directive has been removed. To compose a prompt in $EDITOR (via
Ctrl+G) and then review or tweak it in sase's TUI prompt bar instead of launching
immediately, end any line of the editor buffer with the exact suffix @ (a space
followed by @):
Refactor the parser module to use dataclasses. @
This is an editor-return syntax, not a runtime directive — it is only recognized in text
handed back from the external editor. Typing @ directly in the prompt bar and
submitting has no special behavior, and the marker carries no meaning in CLI, mobile, or
workflow execution. Submitting %edit raises a DirectiveError pointing here rather than
silently launching; %e is the %effort alias.
When at least one line ends with @, the marker is stripped from every matching line
and the cleaned text is loaded back into the prompt input bar; the agent is not launched
until you press Enter there. The returned text loads with editor-file semantics: real
multi-agent --- segment separators (outside fenced blocks and leading YAML
frontmatter) split sase's TUI prompt stack into one editable pane per agent segment, and
any leading macro frontmatter is lifted into the prompt properties panel above the top
pane. Because the strip runs before this parsing, a marked separator line such as
--- @ becomes a real --- separator. See Prompt Stacks in the
sase's TUI docs for the full review flow.
Plan Approval and Coder Follow-up¶
SASE's planning workflow is driven by the /sase_plan skill together with the
sase plan approval pipeline. An agent drafts a plan and submits it with /sase_plan
(or sase plan propose). In an agent-runner context, submission hands the session to a
processless plan gate turn and ends the planner turn; that turn, not the provider
process, owns the pending review. In the TUI the authored TALE or EPIC plan status
settles when the selected branch's commands complete.
Feedback launches a replanner; tale approval launches a coder; epic approval may be
terminal because its approval command launches epic execution itself. The %auto:tale
and %auto:epic modes use the same pipeline but answer the plan decision synchronously
when the spelling covers the plan's tier; a plan of the other tier parks for review.
Once the plan is approved, sase launches a follow-up coder agent. That automated
hand-off still inlines the approved plan with @ and does not share a body with the
#coder built-in macro (see
sase/macros/coder.md).
#coder instead takes the approved plan file as its plan_file input, names it by its
YYYYmm/<name>.md reference, and asks the agent to locate and read the plan itself (for
example with sase artifact read plan:<reference> "<reason>") rather than receiving it
pre-inlined. The coder starts with a fresh context window; the plan file is the hand-off
artifact and the coder does not inherit the planner's chat transcript. The coder prompt
also carries a %model: directive. A model chosen at approval time (or a %model:/%m
directive inside a custom coder prompt) wins. When no model is chosen, the follow-up
validates the tale plan it will actually hand off and routes by that plan's size:
%model:@xsmall, %model:@small, %model:@medium, %model:@large, or
%model:@xlarge. Tale and commit approvals publish the reviewed plan to the durable
archive before their response becomes terminal. The follow-up receives the canonical
plan: reference and resolves it in its own workspace instead of depending on the
approver's checkout path. If that required publication fails, SASE does not write the
terminal response, so the approval stays pending and actionable. Tale plans without size
metadata normalize to @medium. The recorded follow-up metadata resolves
the alias to the concrete model the coder actually launches with and keeps the size
alias as ← @<size> launch provenance in the coder's Model: field.
Outside the TUI, sase plan shows the same pending PlanApproval notifications plus
recent approved and inferred rejected archived plans. Use the plan name from a Proposed
row with sase plan approve <name> to default to a tale; epic-authored plans require an
explicit --kind epic or --kind tale override. The same command can approve a scratch
or archived plan that has no live gate, launching #coder in the planner's agent
session when it is safe to attach or standalone otherwise; --dry-run previews this
without changing state, and direct approvals leave durable receipts. Approving an
already-approved plan whose coder failed, was killed, or never launched relaunches a
replacement coder against the same approval; a plan whose coder is still running or has
finished is refused instead. If another responder answers the stale gate while the
command runs, sase launches no second coder and reports what it committed, exiting 1
with recovery commands when a tale coder is still owed. The approve kind runs the
coder without committing an SDD plan; tale commits an SDD tale and runs the coder;
epic commits the matching SDD tier and launches the bead follow-up; commit records
the approved plan in SDD without launching a coder. -m/--model picks the follow-up
agent's model, while -p/--prompt adds extra coder instructions for the approve and
tale paths. Tale and epic approvals validate the target schema first and leave an
invalid proposal pending. CLI rejection writes the same no-feedback rejection response
as sase's TUI, then attempts to dismiss and user-kill the matching planner when it can
be found.
When an agent launched with %auto:epic later submits an epic plan with /sase_plan or
sase plan propose, sase follows the same epic path as the TUI Epic action: it writes
the SDD epic files, commits them as needed, initializes beads, and launches the epic
follow-up agent. Like every other %auto spelling, %auto:epic still answers question
gates automatically with the first option; it only limits which plan tier auto-resolves.
%auto:epic
%id:billing-epic
Plan the billing dashboard epic.
Repeat Directive¶
The %repeat directive runs the same prompt multiple times. The argument is a positive
integer specifying the repeat count:
%repeat:3
%id:linter
Run lint checks on the codebase.
This launches 3 independent agents — each spawned with its own process, workspace, and
agent_meta.json, appearing as its own top-level entry in the Agents tab. Fan-out
happens at launch time: the directive is consumed when the agents are spawned, so there
is no outer loop or TUI affordance ticking through iterations. The slot numbers are
appended to the %id base (linter.1, linter.2, linter.3); when %id is omitted
the auto-assigned base is used (e.g. a.1, a.2, a.3).
Iterations run sequentially: all N agents are spawned up front and register
immediately in the TUI, but iteration k+1 is automatically wait-chained behind
iteration k via an injected %wait:<prev_name> directive. This turns %repeat into a
serial iteration primitive — each iteration can observe its predecessor's work — without
blocking the launcher on any single agent.
Each iteration exposes two iteration-scoped named arguments in the agent's workflow:
| Variable | Meaning | Example with %repeat:5 |
|---|---|---|
n |
Current iteration (1-based) | 1, 2, 3, 4, 5 |
N |
Total iterations (the %repeat argument) |
5 |
These are threaded through via the SASE_REPEAT_ITERATION and SASE_REPEAT_TOTAL
environment variables — the agent runner reads them, converts to ints, and passes them
as named args into the workflow so they appear as Jinja2 variables in the prompt body:
%repeat:5
Run test suite batch {{ n }} of {{ N }}.
Stopping a repeat chain early with STOP¶
A repeat iteration can stop every later slot by setting the reserved STOP output
variable before it completes:
%repeat:5
Process the next batch; if there is no more work, run: sase var set STOP=1
Because the slots are already spawned and wait-chained, "stopping" works on wake: when a
later slot's %wait on its repeat predecessor resolves, the slot checks that
predecessor's STOP output variable. If it is truthy, the slot propagates STOP,
finalizes as a successful completed (skipped) slot — recording
repeat_stopped: true and stopped_by in its done.json — and exits without claiming
a workspace or running its prompt. Keeping the outcome completed lets the stop cascade
down the chain through the ordinary %wait resolution, so each remaining slot winds
down one wait-check cycle after the previous one. STOP is conservative: "", 0,
false, no, and off (case-insensitive) are not-stop; any other value stops the
chain. See Cross-Agent Output Variables for how STOP
behaves outside repeat chains.
Alt Directive¶
The %alt directive splits a single prompt into multiple variant prompts, each launched
as a separate agent. Each branch replaces the directive span in the output prompt.
The preferred shorthand is %{A | B | ...}, which uses braces and splits branches on
top-level | separators:
%{#review | #test | #docs}
Analyze the codebase.
This produces three agents, each with "Analyze the codebase." but with #review,
#test, or #docs substituted in place of the directive. Branches can be arbitrary
text — macro references, directives, plain instructions, or [[text blocks]]. Because
branches split only on a top-level |, a comma is ordinary branch text:
%{foo, bar | baz} is two branches (foo, bar and baz), not three. Nested (),
[], {}, and backtick-quoted spans are not split, and any | inside them is treated
literally.
You can also fan out just a directive value by putting the alt group after the
directive's colon. For example, %effort:%{medium | high | xhigh} expands into three
launched prompts with %effort:medium, %effort:high, and %effort:xhigh;
%m:%{opus | sonnet} is equivalent to %{%m:opus | %m:sonnet}.
Where %{...} Opens¶
%{ opens an alternation anywhere outside literal/definition zones: at the start of a
line, after whitespace, mid-word (foo%{bar | baz}qux launches foobarqux and
foobazqux), after punctuation (pre-%{a | b}), right after another group
(%{a | b}%{c | d}), inside a directive value (%m:op%{us | x}), and inside another
alternation's branch (%{sase-%{core | github} | chezmoi} launches sase-core,
sase-github, and chezmoi). A nested group expands only when its branch is selected.
The only way to write a literal %{ is inside inline code, fenced blocks, or
%macros_enabled:false regions.
The forms %alt(...) and %(...) open only at a directive-valid position (start of
line, or after whitespace, (, [, {,
", ', or :). That keeps %(name)s format strings, 50%(approx), and
--format=%(refname) ordinary text.
A branch glued to neighboring text keeps adjacent directives parseable: when a selected,
non-empty branch starts with a % directive marker and the character before the group
is not a directive boundary, the renderer inserts one space before the branch, and
likewise after a trailing %name… directive when the next character is a word
character. So Review:%{%m:opus | %m:sonnet} launches Review: %m:opus with the opus
model, and foo %{%m:opus | %m:sonnet}bar launches foo %m:opus bar. #macro
references and +tags in a glued branch are substituted verbatim, so put whitespace
before the %{ to keep them references.
One ambiguity to know: a literal % immediately followed by a Jinja {% tag
(100%{% if x %}) now reads as an alternation opener. Write 100% {% if x %} or use
inline code instead. Conversely, a %{, %( or %alt( right after a literal {
({%{a | b}) is an alternation, not a Jinja tag. It launches {a / {b.
The forms %alt(...) and %(...) are accepted; both use
parentheses with comma-separated branches:
%alt(#review, #test, #docs)
%(#review, #test, #docs)
New prompts, completions, snippets, and docs should prefer %{...}. %(...) stays
parse-compatible during the migration; it may be removed in a future release.
Named Branches¶
Branches can be named with id=value. The value is inserted into the spawned prompt
and the id becomes the child agent suffix. For example,
%id:review %{sec=[[security]] | perf=[[performance]]} launches review.sec and
review.perf. Unnamed branches use numeric suffixes while skipping any numeric ids
already provided by named branches, so %{2=[[named]] | [[first]] | [[second]]}
launches suffixes 2, 1, and 3.
When the same named branch id appears in more than one alt directive, those directives are correlated: values with the same id render into the same child prompt, and a missing id in one correlated directive renders as empty text. Empty renders also collapse adjacent horizontal whitespace, so they do not leave doubled spaces or spaces before punctuation; only spaces and tabs are collapsed, newlines and indentation are preserved, and non-empty branches are untouched. For example:
%id:repo %{a=Describe | b=Explain} how this repo works %{a=in detail}.
This launches repo.a with "Describe how this repo works in detail." and repo.b with
"Explain how this repo works.".
Single Branch (With/Without Split)¶
A single-branch alt is treated as a with/without split — it produces two prompts: one with the branch text and one with the directive removed entirely:
%{Also check for security issues.}
Review this module.
This launches two agents: one with "Also check for security issues. Review this module." and one with just "Review this module."
Cartesian Product¶
Multiple alt directives can appear in the same prompt. Branch lists with no repeated named ids form a Cartesian product: one agent is launched per combination. Brace and paren forms mix freely:
%{Focus on security | Focus on perf} %{%m:opus | %m:sonnet}
Review this code.
This produces 2 × 2 = 4 agents (every focus area paired with every model). Model
directives used as branches inside %{...} participate in the Cartesian product
naturally: %{#review | %model:opus} fans out a default-model review branch and an opus
branch.
Repeated named ids are the exception to the Cartesian rule. Disjoint named ids and unnamed branches remain Cartesian; only the same explicit id repeated across directives is zipped together.
Multi-Model Fan-Out¶
The %model directive is single-value. To launch multiple agents in parallel — one per
model — put one model directive in each %{...} branch:
%{%m:opus | %m:sonnet}
Review this code for edge cases.
This launches two agents with identical prompts, each using a different model. Each
agent appears as a separate entry in the Agents tab. Each branch accepts one model.
Comma-separated model arguments and repeated top-level %model directives are rejected.
Colon syntax (%m:opus) and single-model parentheses (%m(opus)) launch a single agent.
When a prompt fans out to multiple models, the spawned agents share a single base name
and carry a runtime suffix so they can be told apart at a glance. Given
%{%m:opus | %m:gpt-6.1-sol} %i:foo, the two agents are named foo.cld and foo.cdx.
The runtime suffix is a short alias declared by the provider plugin (via the
llm_provider_short_name hook) — cld, cdx, agy, qwn, opc for the built-in
providers — falling back to the full provider name for plugins that don't declare one.
If %id is omitted, a single auto-generated base is allocated and shared (e.g. a.cld
/ a.cdx) rather than each agent picking its own letter independently. Single-model
prompts retain their plain %id value unchanged. When two models share a runtime (e.g.
%{%m:opus | %m:sonnet} — both claude), the model name disambiguates the suffix:
foo.cld-opus and foo.cld-sonnet. Long model slugs are replaced with a short alias
declared by the provider plugin, so a same-runtime agy fan-out can read as
foo.agy-flash36h / foo.agy-flash35h rather than echoing the full model string. Model
arguments used for naming are first resolved through macro shorthand expansion, while
the launched prompt keeps the original %model value. For example,
%i:ag %{%m:#flash | %m:#pro} can launch agents named ag.agy-flash35h and
ag.agy-flash36h.
Multi-Value Directives¶
The %wait directive supports multiple occurrences — each adds to the wait list:
%wait:agent1
%wait:agent2
%wait:agent3
Do work after all three agents finish.
Agent dependencies, time floors, and per-launch capacity budgets can be mixed freely —
dependencies and the time floor stay on %wait, while the admission budget moves to
%queue:
%wait(agent1, time=5m) %queue(capacity=1)
Wait for agent1 to finish, wait at least 5 minutes from launch, then wait until occupied
weighted load plus this launch's weight fits within its authored budget of 1.
Related¶
_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.