fractal node#
The fractal node sub-app manages individual agent nodes: creating them,
launching and signaling their iteration loops, merging and tearing down their
branches, inspecting their state, and tracking their time and cost budgets.
Tree-wide operations (fractal pause, fractal resume, fractal reset,
fractal destroy) live on the top-level CLI — see Top-Level Commands. The
lifecycle model behind the verbs is described in Node Lifecycle, and
the iteration loop each node runs in The Iteration Loop.
Conventions#
Every command below follows the same shape:
- Target selection
Most commands take an optional
NODEargument naming the target node’s branch. When omitted, the target is the node at--path(default.), resolved to its git-worktree toplevel. From the repo root, the checkout’s own user node is the target whenever the checked-out branch carries one (the normal case on the tree’s root branch). On a checkout with no user node, the caller’s own node is used when the_NODEenvironment variable is set (as it is inside a node’s loop); otherwise a single worktree under.worktrees/resolves implicitly (echoed on stderr), and multiple worktrees refuse with a request to name one.- Short names
Wherever a branch is accepted, a unique trailing segment works:
lexerresolves tomain.parser.lexer. An ambiguous short name refuses and lists the candidates; an unknown one refuses withNo node found for branch: '<name>'.--pathEvery command accepts
--path <dir>(default.). It identifies the caller’s worktree — pick the target with theNODEargument, not with--path. The one exception isnode init, where--pathnames the project root (repo root or monorepo sub-project).- Output
Listing commands print a text table on a TTY and CSV automatically when piped;
--csvforces CSV either way, and--json(where present) prints a JSON array instead ([]when empty, mutually exclusive with--csv). Empty listings still emit the header row. stdout carries the parseable result; notices, warnings, and confirmations ride stderr. Core refusals printError: <message>on stderr and exit 2, as do argument errors raised at the CLI boundary (bad values, unknown or ambiguous node names, mutually exclusive flags), which print a usage error (Invalid value: <message>). Exit 1 is reserved for a command’s own nonzero outcome —fractal commit --checkon a dirty tree — and for a declined interactive confirmation (deletewithout--forceprintsAborted.). Whatever the cause, the last stderr line of every failed command isFAILED (exit <code>)naming the exit code, so atail -1can never mistake a failure for a success.- Values
Numeric caps must be non-negative; unlimited is expressed by omitting the flag, never by a sentinel value. Durations take a unit suffix —
<number><s|m|h|d>, e.g.30s,10m,1h— and must be at least one second. Costs are USD amounts printed as$X.XXXX.
The underscore-prefixed commands are internal plumbing invoked by the node’s
own scripts; they are not user surface and are hidden from --help.
Creating nodes#
init#
$ fractal node init NAME [options]
Create an agent node: a git worktree on branch <parent>.<name> under
.worktrees/, plus a node data directory .fractal/<branch>/ seeded with
steps, scripts, skills, and config.json (a template-seeded node also
records what seeded it in _template.toml). The node’s task contract
lives in <node_dir>/NODE.md — author its Instructions and Completion
Requirements sections, then launch with fractal node start.
With a template, supply required inputs before init. Reusable prose and Jinja
expressions live in NODE.md, steps, or source-only fragments such as
_partials/foundations.md. Jinja includes read only the same committed
template bundle. Its optional _template.toml holds literal [values]
defaults, while the node’s generated file records the complete resolved inputs
and source commit. TOML is parsed as data, without rendering, and metadata is
not a standalone output. An explicit Jinja include treats any named bundle
file as source.
Jinja supports includes, imports, macros, inheritance, branches, loops, and raw
blocks. Source line endings normalize to LF, trailing newlines are retained,
and HTML escaping and automatic block whitespace trimming are disabled. Values
containing {{...}} remain literal; $VAR remains for runtime rendering.
Tables render in recursively sorted key order; lists preserve their supplied
order. Only selected outputs render independently, and all rendering completes
before deployment.
Rendered step frontmatter must satisfy the step configuration grammar before
deployment. Use {{ detached | lower }} or {{ detached | tojson }} for
a boolean flag: the step parser requires lowercase true/false.
NAME is one branch segment: letters, digits, and underscores only (no
dots, dashes, or slashes), at most 64 characters. A node spawned from inside
another node’s worktree nests under that node; otherwise it nests under the
node at --path.
Option |
Default |
Meaning |
|---|---|---|
|
|
Project root: repo root or monorepo sub-project. |
|
de-slugged name |
Human-readable display name. |
|
unrestricted |
Subdirectory scope(s) within the worktree that commits are restricted
to (comma-separated; repeatable). Must be repo-relative — absolute or
|
|
parent branch |
Branch to start from; when set it is also the squash-merge target and must have a checked-out worktree. |
|
none |
Target node branch for meta-configuration; expands to
|
|
package seed |
Seed surfaces from the parent instead of the package seed
(comma-separated; repeatable): |
|
package seed |
Template folder: any tracked folder holding a |
|
whole template |
Deploy only these template-relative output paths (repeatable; a
directory entry covers its subtree; no globs); requires |
|
none |
Skip deployment of these template-relative output paths; the mirror
image of |
|
none |
TOML inputs for seed-time Jinja rendering, overriding the template’s
optional |
|
none |
One input as a TOML literal (repeatable); wins over |
|
none |
Commission pin: must resolve to a commit, and every |
|
nearest ancestor’s |
Agent command, validated against the agent registry (a typo refuses). See Agent Backends. |
|
inherited, else vendor-native |
Provider route (e.g. |
|
agent’s default |
Model override, passed to the agent CLI. |
|
none |
Reasoning-effort override, passed to the agent CLI. |
|
unlimited |
Per-run iteration cap; must be greater than 0. |
|
unlimited |
Maximum child nesting depth below this node. |
|
unlimited |
Maximum unsettled direct children (concurrency, not lifetime count). |
|
unlimited |
Maximum unsettled descendants in the subtree. |
|
none |
Whole-run time budget (e.g. |
|
none |
Per-iteration time budget; defaults to |
|
none |
Per-step time budget. |
|
|
Retries per failed step; |
|
|
Delay before each step retry. |
|
none |
Fixed iteration schedule; mutually exclusive with |
|
none |
Delay between iterations. |
|
|
Sleep between sync passes while waiting — on a child drain or an approval gate. |
|
uncapped |
Per-run cost ceiling; each launch arms the cap anew. |
|
none |
Per-iteration cost cap; requires |
|
none |
Per-step cost cap; requires |
|
|
Budget reserved for cleanup, as USD or a percentage of |
|
enabled |
Run sync mode (the radio check-in) before each step. |
|
continuous |
Run each step as a separate agent invocation instead of one continuous session. |
|
disabled |
Skip pushing to remote after each iteration’s commit. Inherited from a local parent and immutable once set. |
|
disabled |
Subscribe to no radio channels (the parent still reads this node). See Radio. |
|
disabled |
Seal the node’s mailbox: its own seat cannot read hosted messages until
an operator or the parent unseals it ( |
|
— |
Delete node files and reinitialize. |
Cost caps must satisfy step <= iter <= run ordering. Spawn caps
(max_children, max_descendants, max_depth, and the parent’s
remaining budget) are enforced at init, and spawning refuses under a paused
ancestor or a tree-wide pause latch. An existing node refuses without
--reset, and --reset itself refuses over an active or paused node.
One advisory rides stderr without blocking: a node with neither
--max-cost nor --max-iters warns that it can run and spend without
bound.
$ fractal node init parser --title "Build the parser" --scope src,tests \
--max-cost 10 --max-iters 10 --timeout 24h
$ $EDITOR .worktrees/main.parser/.fractal/main.parser/NODE.md
$ fractal node start parser
Running nodes#
start#
$ fractal node start [NODE] [--continue] [--clean] [--drain]
[--max-cost <usd>] [--headless | --tmux]
Launch the node’s iteration loop in a tmux session (named
<repo> (<branch>) with dots flattened to dashes) or, with --headless,
in a detached process group. Run parameters come from the node’s
config.json; only the runtime choice and the continue flags are set at
launch time.
--continueRe-arm a settled node (
completed,stopped,exited, orkilled) for further iterations. The launch restores the worktree; a dirty worktree refuses and lists the uncommitted project files that would be discarded.--cleanWith
--continue: acknowledge discarding uncommitted project files (node-directory files are exempt).--drainWith
--continue: run a drain — the harness forbids spawns and re-arms from this run (node init— a spawn or a whole new tree — plusstart/update/resumeand the_loopentry they front all refuse from its seats, enforced via the exported_DRAINand the run’s own recorded drain) and injects the DRAIN mode doc so every seat closes out instead of expanding. The drain’s own relaunch after a park is exempt.--max-cost <usd>With
--continue: retune the cost cap before relaunch, echoedmax_cost: old -> new. Required when the last run ended on its cost budget — runs are isolated and each launch arms the cap anew, so a budget-ended run refuses a bare--continue.--headless/--tmuxSelect the runtime backend:
--headlessruns the loop in a detached process group, appending its output to the node’sheadless.logwith one=== Launched ... ===banner per launch (truncate the file when it grows; delete it only while the node is not running — the live loop keeps writing to a deleted file’s inode, unseen). The backend is sticky: with neither flag norFRACTAL_HEADLESSset, a launch reuses the backend the node last launched with (tmux for a node that has never run headless), and the flags force and re-record it. Seats always export the parent’s backend inFRACTAL_HEADLESS(exactlytrue/false), so a delegated child start follows its parent unless the seat passes a flag.resumealways relaunches through the recorded backend — the flags andFRACTAL_HEADLESSdo not apply to it.
A fresh start requires status exactly idle. Starting refuses on: the user
node; a retired node (unretire first); a paused node (resume first); a
paused ancestor or tree-wide pause latch; a foreign tmux session already
holding the node’s session name; a node whose recorded process group is
still alive (identity-verified, so a recycled group id never blocks); and a
stored config the launch re-validation rejects. Continuing from killed
surfaces the recorded kill attribution as a notice. An uncapped start is
allowed but warns loudly.
$ fractal node start parser
$ fractal node start parser --continue --clean --max-cost 10
attach#
Attach the current terminal to the node’s tmux session. Requires the node to
be active. A headless node refuses and names its headless.log to
follow instead.
$ fractal node attach parser
Signals#
Three graceful verbs and one immediate one end or suspend a running node. The
graceful verbs — finish, stop, pause — require the target to be
active with a run; kill also accepts paused and idle nodes —
a booting node (live session, active not yet stamped) is reaped, and a
never-started spawn is stamped killed. Each accepts --reason <text>,
recorded with the signal. Refused signals are recorded as failed events in
the activity log.
finish and stop#
$ fractal node finish [NODE] [--reason <text>] [--cancel]
$ fractal node stop [NODE] [--reason <text>]
finish ends the node gracefully after the current iteration; stop
ends it after the current step. Both are queued signals the loop polls at
its boundaries: a stop that lands mid-step waits for the in-flight agent to
complete — however long that takes — and never signals or tears it (the
immediate path is kill).
Warning
Both verbs cascade to the node’s entire subtree of active descendants
(children first, so the subtree drains before the target settles). There
is no single-node form: stopping a manager stops every lane under it, each
after its own current step. Invoked on the user node, finish/stop
broadcast tree-wide (the user node itself carries no signal).
--cancel (finish only) withdraws this node’s pending finish signal
instead of sending one — it never fans out, and refuses when no finish signal
is set.
$ fractal node finish parser --reason "requirements met"
$ fractal node finish parser --cancel
pause and resume#
$ fractal node pause [NODE] [--reason <text>]
$ fractal node resume [NODE]
pause suspends the subtree: the pause signal lands parent-first, each
in-flight agent invocation is aborted, and the loops park with status
paused, leaving their run and iteration rows open. A parked node has no
tmux session — that is its normal state, not a crash. While a node (or the
whole tree, via fractal pause) is paused, spawning and starting refuse
anywhere beneath it; the only legal verbs on a paused node are resume,
kill, and chat.
resume relaunches the parked subtree leaf-first. Each loop adopts its open
run — same budgets and iteration count, the interrupted step re-entered
(resuming the recorded agent session when one exists) — and run and iteration
deadlines are credited for the paused span. A node still parking gets its
pending pause withdrawn instead. Resuming refuses when the node is not paused
or when an ancestor is still paused.
$ fractal node pause parser --reason "investigating a regression"
$ fractal node resume parser
kill#
$ fractal node kill [NODE] [--reason <text>]
Kill the node immediately: descendant sessions and recorded process groups
are reaped first (re-enumerated to a fixpoint, so mid-sweep spawns are
caught; a descendant refused over a launch claim in flight is retried within
a bounded budget once the claim resolves), then the node’s own, and open
rows are marked killed. The reap covers the loop runtime (the tmux
session, or a headless or bare loop’s recorded process group) and any step
group the loop recorded. Killable states are active, paused, and
idle — a booting node is reaped and a never-started spawn is stamped
killed so it can never activate. Two other refusals guard the reap, each
before any kill state is written: a recorded process group whose identity
the ps probe cannot verify names ps -p <pgid> and the record file to
remove, and a record naming no process group yet — a launch claiming it —
asks for a retry once its pid lands (or the record’s removal if no launch is
running) instead of sweeping the claim. The attribution
killed by <actor>[: reason] lands on the event, the signal, and the run
row, and is surfaced as a notice on a later start --continue.
$ fractal node kill parser --reason "runaway loop"
Integration and teardown#
merge#
$ fractal node merge [NODE] [--continue] [--ignore-scope]
[--validate <script>] [--delete] [--force|-f]
Squash-merge the node’s branch into its merge target — the configured
base when set, else the dotted parent. One squash commit lands on the
target; the node’s full history stays on its own branch. Merging refuses on
the user node, when the node itself is active or paused, and when the
target is active or paused (except from inside the target’s own
loop, its normal child-merge path). Non-fatal warnings ride stderr.
Before committing, the merge holds the squash to the node’s commit scope: the
staged paths outside .fractal/ are judged by the node’s scope roots
and its project wiki/, with the worktree-root .gitattributes admitted
only as init’s own **/_index.md merge=wiki edit (a repo-root node without
a scope is unrestricted; a sub-project node without one is bounded to its
project directory) — the same law fractal commit applies — and a path
outside them refuses the merge. The refusal names the paths and both
remedies: widen the scope with fractal node config set scope=<dirs>
--path=<node worktree> and commit it with fractal commit "widen scope"
--path=<node worktree> (an uncommitted config change makes the rerun skip
the merge-base advance), or rerun with --ignore-scope. A fresh merge
restores the target on refusal. A --continue leaves the staged squash in
place, and its remedies differ: re-run with --continue --ignore-scope, or
widen the scope (config set, then commit) and redo the squash (git -C
<target worktree> reset --hard HEAD && git -C <target worktree> merge
--squash <branch>), because the widening commit is a node commit made after
the hand squash, which makes --continue refuse.
Nothing under any .fractal/ directory on the target changes except a
scope root of the merging node that is, or lies under, a .fractal/
directory (a --meta node’s scope is the target’s seed directory): every
other such path the squash changed returns to the target’s HEAD — a path the
target tracks is restored to its content, a path it does not track is
removed — with a warning naming each, so the squash never adds the node’s own
seed or its descendants’ seeds. On the user node’s branch a copy the branch
already tracks is stripped as well (the root owns no seed, so it is a leak);
a node target keeps a copy it already tracks — its PREPARE git merge
--no-ff of the child put it there — until it merges upward itself, so a
child whose advance was skipped never inherits a deletion of its own live
seed on its next merge of the parent. The merge-base advance then brings the
target’s tree into the node’s worktree, so the node’s copy of a dropped path
survives only in its branch history (git -C <node worktree> log
--full-history -- <path> lists the advance that dropped the path first and
the node’s own commit below it; git show <commit>:<path> on that lower
commit, or git show <advance>^:<path>, reads the copy; a plain git
log follows the target’s side of the advance and lists nothing). A fresh
merge refuses before the squash when it would write over any file that exists
untracked on the target’s disk — an ignored private file such as
local.env, or the user node’s own live seed, self-ignored on the root but
committable from a child — judged over every path the node added or changed
since the merge-base that the target’s HEAD does not track, a file sitting
where the squash would create a directory included (the prefix walk stops at
a path the target’s HEAD tracks: a tracked file the node replaced with a
directory is the squash’s own type change, not a collision); the refusal
names the files to move aside or drop from the branch. When the target is the
user node, the root’s committed tree is judged before the squash for seed
directories of the root’s own dotted nodes (.fractal/<target>.*/, so a
--base merge into another tree’s root judges that root) — the root owns
no seed, so every one is a leak. Exactly what the strip removes — the merging
node’s seed at its own project prefix and its descendants’ seeds at any
depth — is named in one warning (tracks seeds of <branch> or its
descendants, leaked by an earlier merge: <dirs>; this merge removes them);
the rest, a same-named copy of the node’s seed under another project prefix
(the node re-created at a different project path) included, get a second
warning with a git -C <target worktree> rm -r -- <dirs> && git -C <target
worktree> commit -m 'drop leaked node seeds' remedy line that removes them
from the tree and from the root worktree’s disk (the copies are never live
seeds; those sit in each node’s own worktree). A node target is not judged:
its branch legitimately carries other nodes’ seeds (its ancestors’ by fork,
its descendants’ by PREPARE merges, a sibling’s by the merge-base advance).
The check reads the committed tree, so a --continue never reports the
hand-staged seed. Whether the target is the user node is read from the repo’s
record of the target branch, so a root checked out in a linked worktree is
still stripped and leak-checked; a direct merge.sh call that cannot read
the target’s node config warns could not read <target>'s node config;
treating it as a node target instead of silently treating it as a node.
Conflicts only under .fractal/ outside the node’s scope roots resolve to
the target’s content — a warning names them — and the merge continues; any
other conflict fails and restores the target (the “restored” verdict is
judged by the target’s state, not by git’s exit code; when it is not
restored, the error names the git -C <target worktree> reset --hard HEAD
to run before merging again). A squash that dies after git wrote the index (a
stale or unwritable SQUASH_MSG) is reset and its squash markers cleared,
and the error reads merging <branch> into <target> failed after staging;
the parent worktree has been restored; resolve and retry (or its could
NOT be restored form, judged the same way); failed before staging
anything is reserved for a squash that staged nothing — git’s own refusal
over a plain untracked file, or another git process holding the target’s
index. An interrupt before the squash commit restores the target the same
way and marks the merge event failed (one that lands while the event is still
being opened closes it as failed too); once git commit has moved the
target’s ref the squash has landed, and the merge finishes it — the
merge-base advances, the event closes as completed, and the command prints
Squash-merged ... and exits 0 — rather than reporting a restore. An
interrupt during the advance finishes the node’s worktree update or rolls it
back, warning only when an advance was underway; one during a no-op merge’s
bookkeeping prints that arm’s own summary (Nothing to merge: ...) with no
advance warning. After the commit, the node’s merge-base advances with a
two-parent commit on the node’s branch (merge <target> (post-squash))
whose tree is the target’s post-squash tree with the node’s own seed and its
descendants’ seeds kept, so the node’s worktree converges to the target and a
later merge only diffs new work. The advance is skipped with a warning when
the node’s worktree is dirty, on another branch, or holds an untracked or
ignored file or directory in the way of a path the target now tracks — a
rename on the target that lands where the node keeps a private file, or an
untracked case-only alias on a case-insensitive filesystem, included, while a
path the node tracks under a different case is not a collision (the update
renames it), and neither is a path the node tracks at, under, or above the hit
(the target turned a file into a directory or back — a type change the update
performs). Move the file aside; the next merge that lands work advances it (a
fresh merge offering nothing exits at “Nothing to merge” before the advance —
unless the restore dropped a .fractal/ change outside the node’s own seed
and its descendants’, the paths the restore warnings name, or a .fractal/
conflict outside them resolved to the target’s content, either of which still
advances the merge-base so the node converges and the warning does not repeat;
a node whose only offering is an edit to its own seed exits without advancing,
and its next work merge advances it); a failed worktree update rolls the
worktree back before skipping. A git read that fails during the advance (the
node’s worktree stops answering) skips it with a warning rather than failing
the landed merge, and an edit to a tracked file the commit law’s excludes hide
(a force-added lock or status file) counts as dirt, so the update never
overwrites it.
--continueFinish a hand-resolved squash after a conflicted merge: redo
git merge --squashin the target worktree, resolve and stage the conflicts, thenmerge --continueruns the merge’s own tail —.fractal/restore and seed strip, footprint check, index refresh, commit, merge-base advance. The squash must be fully staged: unstaged tracked changes in the target refuse the continue — save any copy you need, stage (git add) the paths that belong to the resolution, and discard the rest (git -C <target worktree> checkout -- <path>); the merge restores every.fractal/path to the target’s HEAD anyway. It must also cover the node’s current tip: a commit the node made after the hand squash (an iteration, a nested child’s merge) refuses the continue and names the redo —git -C <target worktree> reset --hard HEAD && git -C <target worktree> merge --squash <branch>. The fresh merge’s untracked-file check does not run here, so a--continuecannot prevent the overwrite of a file that exists untracked on the target’s disk: git’s own squash refuses over a plain untracked file but writes over an ignored one (git treats it as expendable) — a file the fresh merge would have refused over — and the hand squash has already done it, so move private files aside before redoing the squash by hand.--ignore-scopeMerge out-of-scope changes instead of refusing — paths outside the node’s scope roots and its project wiki (
.fractal/paths are never judged; the worktree-root.gitattributesis admitted only as init’s ownmerge=wikiedit).--validate <script>Run a repository gate on the destination before the squash commit. Bash runs
<script>, a path relative to the target worktree root, from that root after the.fractal/restore, footprint check, and wiki index refresh — on a fresh merge and on--continuealike (repeat the flag with the continue). The script must exit 0 without changing the staged tree or leaving tracked unstaged changes; the merge stages none of its output. A failing script restores a fresh merge’s target and, with--continue, leaves the staged resolution in place for repair. A no-op merge (Nothing to merge) skips it. An absolute path, a..component, a symlink script, or a path escaping the target worktree is refused.--deleteDelete the node (worktree, branch, and whole subtree) after a successful merge. The teardown’s refusals and its confirmation prompt (which names the descendant count and suggests
retire) run before the squash, so a refusal never lands after a merge that already committed.--force/-fWith
--delete: skip the confirmation prompt.
$ fractal node merge parser
delete#
$ fractal node delete [NODE] [--force|-f]
Recursively remove the node’s subtree, deepest first: worktrees, local and
remote branches, registry rows, and radio subscriptions. History rows
persist — runs, iterations, steps, events, and messages all survive in the
central database. Without --force an interactive confirmation names the
descendant count and suggests retire as the non-destructive alternative.
Deletion refuses on: the user node; a subtree containing any active or
paused node (stop, resume, or kill it first); a caller standing inside a
subtree worktree; and any locked worktree in the subtree. Warnings about
unmerged work ride stderr without blocking: one per node in the subtree whose
branch has commits its merge target never absorbed (each descendant judged
against the deleted node’s surviving target), printed after those refusals
have passed and before the confirmation prompt, and a --meta node’s edits
to its target’s seed directory count as that work.
When the target’s worktree is already gone (removed out of band), delete
<branch> --force takes the orphan path instead: it deregisters the branch
(and any registered descendants) from the registry, refusing if any live
worktree remains in the subtree.
$ fractal node delete parser --force
reconcile#
$ fractal node reconcile [NODE]
The audit step after cleaning up a node’s worktree or branch with plain git
instead of delete: records one orphan event per registry row whose
worktree is gone (already-recorded branches are skipped). Registry rows are
kept. Prints Recorded orphaned node <branch>. per orphan, or No
orphaned nodes to record.
retire and unretire#
$ fractal node retire [NODE] [--reason <text>]
$ fractal node unretire [NODE]
retire parks a node: hidden from list by default and unstartable,
with its branch and history kept. It refuses active/paused nodes,
already-retired nodes (Cannot retire: node is already retired., so the
recorded pre-retire status stays the real one), and the user node. The
pre-retire status is recorded on the retire event, and unretire restores
it (falling back to idle). An idle restore returns the node to the
unsettled pool, so the width and descendant caps are re-checked as at spawn —
refused over cap, with no override.
$ fractal node retire parser --reason "superseded"
$ fractal node unretire parser
Inspection#
status#
$ fractal node status [NODE]
Print the node’s current status, possibly decorated: active (pausing),
active (stopping), or active (finishing) when a signal is pending,
and exited (<reason>) when the latest run recorded why it ended. The read
is self-reconciling — a crashed node, active on record with its loop
runtime (the tmux session, or a headless or bare loop’s recorded process
group) provably gone, is healed to exited first.
$ fractal node status parser
active (finishing)
diff#
$ fractal node diff [NODE]
Show a node’s drift from its recorded template. The recorded folder is
re-rendered at its recorded commit with exactly its recorded input values, the
effective set (the recorded include/exclude listing) is applied, and
each file is compared against the node’s live copy: NODE.md, steps/,
scripts/, skills/, and — for each agents/<agent>/ file — the
live .<agent>/ copy, all in the node’s data directory. A live symlink
and a file the template does not carry are never judged; a template file the
node lacks is drift. Literal braces are valid output and are compared like any
other text. Source defaults are not merged again; deleting a required recorded
input causes a rendering refusal. The recorded commit resolves as long as it
is reachable, so a template folder
moved or deleted later still diffs at the commit that seeded the node.
Drift prints one unified diff per file (template/<file> against
node/<file>); a recorded listing entry the template no longer carries
warns on stderr. The exit code is the contract — 0 clean, 1 drift
(the command’s own nonzero outcome), 2 a core refusal, a node with no
recorded template included — so scripts branch on it rather than parse the
output.
$ fractal node diff parser
list#
$ fractal node list [NODE] [--all] [--retired] [--max-depth <n>] [--json]
[--status <s[,s...]>] [--live] [--count] [--csv]
List a node’s descendants — the listing never includes the target’s own
row (use status for that). Columns, in order: status, detail,
node, title, max_cost, spend, max_depth, max_children,
max_descendants, last, end_reason.
status is always the bare lifecycle word; every qualifier rides
detail, which composes (;-joined) any of: a pending signal
(pausing/stopping/finishing), an exited run’s recorded end
reason, run exhausted: <reason> on a completed run that ended on its
iteration cap and final iteration failed on a drained finish whose last
iteration died (a clean drained finish stays bare), orphaned for a
registered node whose worktree is gone, model drop for an unresolved
served-model divergence, iteration gap <span> for numbers that advanced
with no recorded row, and PAUSED: billing while the newest launches carry
the dead-credits signature. end_reason is detail’s typed counterpart,
a closed vocabulary naming a settled row’s landing: goal_met,
run_exhausted, or final_iteration_failed on a completed row;
cost_budget, timeout, setup_abort, final_iteration_failed, or
other (recorded but unmapped) on an exited row; null when no reason is
recorded (e.g. a reconcile-healed crash) and on every other status. spend
is the current run’s subtree cost at the scope max_cost is enforced at,
blank for a node that has never run. Blank limit columns mean unlimited.
last is the age of each node’s newest activity, with a ! suffix
flagging an active node quiet past max(step_timeout, 5m). On a TTY,
statuses render bracketed ([active]); machine output stays unbracketed.
--allInclude retired nodes.
--retiredShow only retired nodes.
--max-depth <n>Maximum child depth to include (
1= direct children only).--status <s[,s...]>Filter to one or more comma-separated statuses. Valid values:
active,paused,idle,completed,stopped,exited,killed,failed,retired, plus the listing-only pseudo-statusorphan(a registered node whose worktree is gone). Unknown values refuse with the valid list.--liveTrust each child’s real state: relabel a crashed active node — its loop runtime (the tmux session, or a headless or bare loop’s recorded process group) provably gone — as
exited, a booting idle node asactive, and drop nodes whose worktree is gone. This view is read-only; the plain listing instead persists the crash heal and flags worktree-less rows asorphan(or<status> (orphaned)when settled).--countPrint only the number of matching nodes (mutually exclusive with the row formats: a bare number is a shape neither a CSV nor a JSON consumer asked for).
--csvForce CSV output (already the default when piped).
--jsonEmit a JSON array of row objects with typed fields (mutually exclusive with
--csv); scripted consumers should prefer it over CSV scraping.
$ fractal node list --status active,paused
$ fractal node list parser --max-depth 1 --count
$ fractal node list --json
activity#
$ fractal node activity [NODE] [--limit <n>] [--csv] [--json]
Show the node’s lifecycle activity, most recent first. Columns (bind by
header name, not position): timestamp, node, event_id,
step_id, iter_id, run_id, event, actor, step_name,
step, iter, status, exit_code, metadata, duration,
cost. The cost column is the row’s own-node step cost only — subtree
spend, children included, is fractal node cost spent. --limit <n>
bounds the rows returned; --json emits a JSON array of row objects
(mutually exclusive with --csv).
$ fractal node activity parser --limit 10 --json
Approvals#
Steps whose frontmatter sets requires_approval: true run, then gate on
the direct parent before the loop advances: once the step’s agent invocation
succeeds, the child waits for approval, polling (with sync passes paced by
its wait interval) until the approval lands.
approve#
$ fractal node approve NODE [STEP_ID]
Approve a child’s gated step — must be run from the direct parent.
NODE (required) is the child’s branch; STEP_ID defaults to the
child’s active gated step. Approval refuses when the caller is not the direct
parent, when the child has no active step, or when the step does not require
approval.
pending#
$ fractal node pending [NODE] [--csv]
List direct children’s steps awaiting this node’s approval. Columns:
branch, step_id, step, step_name. Only gated steps on
active or paused children appear.
$ fractal node pending
$ fractal node approve parser 1
Step 1 on main.parser approved.
Chat#
chat#
$ fractal node chat [NODE] PROMPT [--session <id>] [--current] [--resume]
[--model <name>]
Send one prompt to a node’s agent and stream the reply. The prompt is
required as the second argument. A bare chat opens a fresh session seeded
with the node’s NODE.md and chat mode, so the agent answers in the node’s
context. Chatting has no side effects on the node’s cost or session records,
and it is one of the few verbs legal on a paused node. The resulting session
id prints on stderr as session: <id> so the thread can be continued.
--session <id>Fork the named session (default: a fresh session).
--currentFork the node’s live loop session — ask about what the loop is doing right now. Mutually exclusive with
--session/--resume; refuses when no live session exists.--resumeContinue
--sessionin place instead of forking it. Resuming the live loop session refuses (it would perturb the loop). Codex agents can resume sessions but cannot fork them.--model <name>Model override (default: the node’s configured model).
$ fractal node chat parser "Summarize your progress so far."
$ fractal node chat parser "Why did the last test fail?" --current
Retuning#
reseed#
$ fractal node reseed [NODE] [--ref <ref> | --template <path[@ref]>]
[--force|-f]
Rewrite a node’s seed surfaces from its recorded template. The recorded
folder is re-rendered at its recorded commit with exactly its recorded input
values, and steps/, scripts/, skills/, and the per-agent files are
rewritten from the result: files the node lacks are added and files it has
are overwritten, so the node matches its template’s effective set (an
excluded file stays gone); nothing is ever deleted, and a live symlink (the
agent auth links) stands. NODE.md, config.json, and memory/ are
never touched, so an operator’s charter edits survive. The reseed lands as
an event on the node’s record, and _template.toml advances to the commit
actually read; the listing is preserved. A bare reseed keeps the recorded
values exactly. An explicit --ref or --template refresh adds the target
template’s defaults only for missing keys, keeping every recorded value,
including values first obtained from a default. It records the complete merged
map; a new required input without a default refuses before live writes.
Changing recorded values does not update generated files until reseeding.
Reseed preserves the charter even when those values or the template change,
so a successful reseed can leave charter drift. Edit NODE.md directly
when the charter needs a change.
Reseed rewrites the live steering surface, so it refuses over an active or
paused node without --force; it always refuses from the node’s own
worktree — a node may not edit its own seed.
--ref <ref>Committish to read the recorded template folder at (default: the recorded commit). Mutually exclusive with
--template. A ref at which the recorded path is no longer tracked (the folder moved or was retired) refuses, naming the path and the ref — re-point with--template.--template <path[@ref]>Re-point the node at another template folder: the new path and the commit read (the node branch’s own tip unless
@<ref>names one) land in_template.toml, and the node reseeds from it — memory and config stay intact, so a moved template is followed with one command. One notice names the@<root>form when the root branch holds a different copy of the folder (a plain or--refreseed stays silent).--force/-fReseed even while the node is active or paused.
$ fractal node reseed parser
$ fractal node reseed parser --template templates/crew@main
update#
$ fractal node update NODE [--title <text>] [--max-cost <usd>]
[--max-iter-cost <usd>] [--max-step-cost <usd>]
[--reserve-budget <usd|N%>] [--step-timeout <dur>] [--max-depth <n>]
[--max-children <n>] [--max-descendants <n>]
Update a child node’s configuration mid-flight — the supported retune
path. It writes the registry row and the child’s config.json together,
and a running loop picks the new caps up at its next iteration boundary. (A
direct edit of the child’s config.json is honored at the same boundary
and back-filled to the registry with a warning.) At least one option is
required; each change is echoed key: old -> new (unset for a prior
null). The merged result is validated exactly as at init.
The options mirror their node init counterparts: --title,
--max-cost, --max-iter-cost, --max-step-cost,
--reserve-budget (USD or N% of the effective max-cost),
--step-timeout, --max-depth, --max-children, and
--max-descendants. When --max-cost changes and the reserve was left
at its default, the 10% reserve is re-derived against the new cap. The user
node cannot be updated (set its config with node config set from the root
worktree instead), and the target’s parent worktree must exist.
$ fractal node update parser --max-cost 100 --max-children 2
max_cost: 10.0 -> 100.0
max_children: unset -> 2
reserve_budget: 1.0 -> 10.0
Time budgets: fractal node time#
time remaining#
$ fractal node time remaining [NODE] [--scope run|iter|step]
Print the seconds left before the next timeout fires, e.g. 600s. By
default the soonest of the configured run, iteration, and step deadlines
answers; --scope narrows to one: run reads timeout, iter
reads iter_timeout (falling back to interval), step reads
step_timeout. Paused spans are credited back to run and iteration
deadlines. Two non-numeric answers are distinct: no limit means no
timeout is configured for the queried scope; not running means a limit
exists but no run, iteration, or step is currently active.
$ fractal node time remaining parser --scope iter
600s
Cost budgets: fractal node cost#
Budgets and spend are per-run: each launch arms max_cost anew, and all
three commands default to the current run (scope to a prior run with
--run <id>). Amounts print as $X.XXXX. A deleted node still resolves
through the tree’s shared history: spent and breakdown answer from its
latest recorded run, while remaining always prints no budget (caps die
with the node).
cost remaining#
$ fractal node cost remaining [NODE]
[--run <id> | --iter <id> | --step <id>]
Print the remaining budget: max_cost minus the run’s subtree spend —
the node’s own steps plus every descendant run chained beneath it. --iter
and --step scope to a specific iteration’s max_iter_cost or step’s
max_step_cost headroom instead; at most one of --run/--iter/
--step may be given. Prints no budget when the relevant cap is unset,
and never goes negative (clamped at $0.0000).
cost spent#
$ fractal node cost spent [NODE] [--run <id> | --iter <id> | --step <id>]
[--max-depth <n>]
Print the total recorded cost, children included; --max-depth 0 restricts
to this node only. --run, --iter, and --step scope the reading to
one run, iteration, or step. Prints untracked when the scope ran only
cost-untracked steps; when ended steps carry no recorded cost, a stderr
disclosure notes how many unpriced steps were excluded.
cost breakdown#
$ fractal node cost breakdown [NODE] [--run <id>] [--max-depth <n>] [--csv]
Print a per-node table — columns node, max_cost, spent — for the
current run (or --run <id>). The target’s own row leads, followed by each
in-subtree descendant; descendants whose registry rows are gone but whose
spend is still recorded append as <branch> (deleted) rows, so the rows
always sum to cost spent.
$ fractal node cost remaining parser
$6.7890
$ fractal node cost spent parser --max-depth 0
$3.2100
$ fractal node cost breakdown parser
Configuration: fractal node config#
A node’s run parameters live in its config.json; the full key reference is
in Configuration.
config get#
$ fractal node config get KEY
Print one config value. Unknown keys refuse with the valid-keys list.
Booleans render as true/false, list values (scope and
clone_dirs) one item per line, and an unset key prints nothing (exit 0).
config set#
$ fractal node config set KEY=VALUE [KEY=VALUE ...]
Write config values, echoing key: old -> new per key. The key=value
form is required; an empty value refuses (use key=null to clear a key).
Types are enforced at the boundary: boolean keys accept true/false/
null; integer keys a non-negative integer (max_iters must be greater
than 0); cost keys a non-negative number — the ceilings (max_cost,
max_iter_cost, max_step_cost) must additionally be positive, with
0 refused by the merged validation, while reserve_budget may be
0; the list keys scope and clone_dirs a comma- or space-joined
list of repo-relative roots (stored in canonical form; . is a legal scope
root but never a clone_dirs entry); every other key stores a literal
string. The merged result is validated — cost positivity and ordering,
reserve range, duration suffixes, pacing exclusivity — before anything is
written; init’s additional flag checks (the agent registry, provider
support, the base worktree) do not run here. Multi-key sets are checked
upfront, so no earlier key lands when a later one is rejected.
A change to the init-fixed keys root, user, and project always
refuses: <key> is fixed at init and cannot be set. (Re-setting the
currently stored value is accepted as a no-op.)
$ fractal node config get max_cost
10.0
$ fractal node config set max_iter_cost=1 sleep=30s
max_iter_cost: unset -> 1
sleep: unset -> 30s