Recipes#
Task-oriented how-tos for operating a fractal tree. Each recipe is
self-contained and assumes an initialized tree with a committed baseline (see
Getting Started); commands run from the repository root unless
noted. Wherever a command takes a node branch, a unique trailing segment of
the dotted branch works as a short name (parser resolves to
main.parser). Full option surfaces live in the Command-Line Interface
reference.
Run a budgeted tree#
Cap a node’s spend at creation with --max-cost and set aside a wind-down
slice with --reserve-budget:
$ fractal node init parser --title "Build the parser" \
--max-cost 10 --reserve-budget 10% --max-iters 10
$ fractal node start parser
--max-cost is a per-run ceiling in USD: every fractal node start
arms it anew, and it covers the run’s whole subtree — the node’s own steps
plus everything its descendants spend, with no slice reserved for the node
itself. Because the budget is subtree-shared, every child spawned under a
capped node must set its own --max-cost, which may not exceed the
parent’s remaining run budget.
--reserve-budget takes a USD amount or N% of --max-cost (default
10%; it must stay below 99% of the cap, and is only meaningful when
--max-cost is set). The reserve is not a hard floor: when the run’s
remaining budget falls to the reserve, the node enters reserve mode —
wind-down instructions join each prompt, telling the agent to commit open
work, settle its children, and report out — and the run ends at the next
iteration boundary.
Two finer caps exist, both requiring --max-cost and ordered
step <= iter <= run: --max-iter-cost caps each iteration, and
--max-step-cost caps each step (a hard cap only for agents that enforce
a budget flag; warn-only otherwise). Starting a node with neither
--max-cost nor --max-iters is allowed but warns loudly — such
a node can run and spend without bound.
While the node runs, watch the headroom and retune mid-flight from the parent:
$ fractal node cost remaining parser
$3.4167
$ fractal node update parser --max-cost 100
max_cost: 10.0 -> 100.0
reserve_budget: 1.0 -> 10.0
A running loop picks up a retune at its next iteration boundary. A run that
ends on its budget lands as exited with exit code 0 — an honest
budget landing, neither a failure nor a completion — and refuses a bare
continue; arm the next run explicitly:
$ fractal node start parser --continue --max-cost 10
See The Iteration Loop for enforcement details and Configuration for the budget keys.
Pause and resume a whole tree#
fractal pause is the tree-wide brake. It latches the tree (a .paused
marker beside the central database), then fans a pause out over every active
node parent-first, aborting in-flight agent invocations; each loop parks with
status paused, its run and iteration rows left open:
$ fractal pause --reason "budget review"
Pause signal sent to 2 nodes (in-flight agents aborted; loops park paused): budget review
While the tree is latched, spawning (fractal node init) and
fractal node start refuse everywhere. Nothing is committed on pause: each
worktree keeps its uncommitted files as the frozen mid-step state that resume
continues from. A paused node has no tmux session — that is its normal
parked state, not a crash — and the only verbs it accepts are resume,
kill, and chat.
Check what is parked, then release the tree:
$ fractal node list --status paused
$ fractal resume
Resumed 2 nodes (parked loops relaunched leaf-first; live pauses withdrawn)
fractal resume lifts the latch, withdraws pending pauses on nodes still
parking, and relaunches parked loops leaf-first. Each loop adopts its open
run: same budgets, same iteration count, the interrupted step re-entered,
with run and iteration deadlines credited for the paused span.
To pause a single subtree instead, use the node-scoped verbs:
fractal node pause <node> [--reason <text>] and
fractal node resume <node>. A subtree resume refuses while an ancestor
(or the tree-wide latch) is still paused. See Node Lifecycle for the
full status model.
See where the money went#
Spend readings are per-run and default to the current run; scope a prior run
with --run <id>. cost spent prints the subtree total (the node’s own
steps plus all descendants); cost breakdown attributes it per node:
$ fractal node cost spent parser
$6.5833
$ fractal node cost breakdown parser
node max_cost spent
----------------- -------- ------
main.parser 10.0 4.1032
main.parser.lexer 5.0 2.4801
main.parser.tests 2.0 0.0
The target’s own row leads, followed by each descendant in the subtree. A
descendant that was deleted but whose spend is still recorded appends as a
<branch> (deleted) row, so the rows always sum to cost spent. Limit
depth with --max-depth (0 = the target’s own spend only); force CSV
with --csv (piped output is CSV automatically).
Cost is recorded, never estimated. When ended steps carry no cost figure, the
sums omit them and a disclosure rides stderr (N unpriced step(s)
(NULL cost) excluded); a scope whose steps recorded no cost at all prints
untracked. A deleted node still answers cost spent and
cost breakdown from its persisted history (its latest recorded run by
default), while cost remaining for it always prints no budget — caps
die with the node, history does not.
Chat with a running node#
fractal node chat sends one prompt to a node’s agent and streams the
reply, without touching the node’s records: no cost lands on its ledger and
no session side effects reach the loop.
$ fractal node chat parser "summarize your progress and current blockers"
A bare chat opens a fresh session seeded with the node’s NODE.md and the
chat-mode instructions — the agent knows its task contract but not the
loop’s working context. To talk to the loop’s own state, fork its live session:
$ fractal node chat parser "which step are you on, and what is left?" --current
--current forks the node’s live loop session: the reply sees everything
the loop’s agent has seen, and the fork leaves the loop itself unperturbed.
It refuses when no live session exists, and codex cannot fork sessions at
all.
Every chat echoes its session id on stderr as session: <id>. Continue a
thread by naming it:
$ fractal node chat parser "and what would unblock you?" \
--session <id> --resume
--session <id> forks that recorded session; adding --resume continues
it in place instead of forking. Resuming the live loop session is refused (it
would perturb the loop). --model overrides the model for this chat only.
Chat also works on a paused node — it is one of the few verbs a parked
node accepts.
Approve a child’s gated steps#
A step file whose frontmatter sets requires_approval: true gates the
loop: after the step completes, the child parks awaiting its direct parent’s
approval before the iteration proceeds. Gate the plan step, for example, so
each iteration’s plan — including any child spawns it proposes — needs
sign-off before execution begins:
---
requires_approval: true
---
## Plan
...
From the parent’s worktree (the repository root for depth-1 children), list what is waiting and approve it:
$ fractal node pending
branch step_id step step_name
----------- ------- ---- ---------
main.parser 10 1 PLAN
$ fractal node approve main.parser 10
Step 10 on main.parser approved.
step_id is optional and defaults to the child’s active gated step.
Approval must come from the direct parent; anyone else is refused. The wait
counts against the child’s run and iteration deadlines (a gated step can time
out waiting), each retry attempt re-arms its own gate, and reserve mode
(budget wind-down) skips gates entirely. See The Iteration Loop for the step
frontmatter contract.
Recover after a crash#
Crash healing is read-driven — there is no separate repair command to run
first. A loop that dies without settling leaves its node active with no
live tmux session; the next read or lifecycle verb that touches it
(fractal node status, fractal node list, fractal node start, and
the teardown verbs) detects that state, stamps the node exited, closes
its open run, iteration, and step rows, and reaps any recorded leftover
process groups:
$ fractal node status parser
exited
$ fractal node start parser --continue
fractal node list --live gives the same authoritative view read-only,
relabeling without persisting. A paused node is never healed: no tmux
session is its normal parked state.
--continue restores the worktree before relaunching: uncommitted project
files refuse without --clean (which acknowledges discarding them), and a
run that ended on its budget refuses without an explicit --max-cost.
If node worktrees or branches were removed out of band — plain git
instead of fractal node delete — reconcile the registry afterwards:
$ fractal node reconcile
Recorded orphaned node main.parser.lexer.
reconcile records one orphan event per registry row whose worktree is
gone (already-recorded branches are skipped) and keeps the rows. Orphaned
rows show in listings as orphan (or <status> (orphaned) once settled)
and can be filtered with fractal node list --status orphan. To drop an
orphaned registration entirely, fractal node delete <branch> --force
deregisters it, refusing while any live worktree remains in its subtree.
Clean up: delete, reset, or destroy#
Teardown comes in three tiers. All three refuse while nodes are running, and
--force only skips the confirmation prompt — it never overrides those
refusals.
fractal node delete <node>Removes one subtree: worktrees, local and remote branches, registry rows, and radio subscriptions. All history rows persist in the central database. Refuses while the node or any descendant is
activeorpaused, and when run from inside a subtree worktree; unmerged-work warnings ride stderr. The confirmation names the descendant count;--forceskips it (and enables deregistering an orphaned, worktree-less node).fractal resetRemoves every node worktree, local branch, and registration in the tree. The user node’s data — config and the central database with all history — plus the wiki and baseline commits survive, and fresh nodes can spawn immediately afterwards. Refuses while any node’s tmux session is alive; paused nodes are killed as part of the confirmed teardown. Also clears a stale tree-wide pause latch. Remote branches are left in place and listed.
fractal destroyThe full inverse of
fractal init: everythingresetremoves plus the user node’s data directory (central database included) and fractal’sinfo/excludeblock. Committed artifacts — the project wiki and baseline commits — and remote branches remain.
For a non-destructive alternative to delete, retire the node instead:
fractal node retire <node> hides it from listings and makes it
unstartable while keeping its branch and history, and
fractal node unretire <node> restores it.