Skip to content
Like what we’re building? Star on GitHub

CLI reference ​

This page maps every public top-level command in the current binary and documents the common operational flags. iterion <command> --help is the canonical, build-specific leaf reference. The global --json flag is inherited by commands; commands that produce structured records use it for machine-readable output.

Command map ​

CommandPurpose
bench asymptoteBuild a workflow-quality stabilisation report from persisted runs.
bench discoveryReport what persisted runs spent on orientation — reads and searches — rather than on the change.
map genRegenerate the committed repository maps (packages, docs + ADR, bots + skills); --check fails on drift.
map buildBuild the repository graph cache — packages, symbols, calls, doc links and every .bot's compiled DAG.
map findFind graph nodes by name, with the file and line to open next.
map neighboursEverything one edge away from a node, in both directions.
map pathShortest directed path between two nodes.
map impactWhat reaches a node — who breaks if it changes.
map rankRank the repository around a set of nodes (personalised PageRank).
botsCreate bots, install published ones, and emit the catalogue.
bundlePack a bundle source directory into a deterministic .botz.
cleanReclaim disk by deleting run worktrees whose work has landed.
completionGenerate Bash, Zsh, Fish, or PowerShell completion.
diagramRender a workflow as Mermaid.
dispatchPoll a tracker and launch an eligible bot per issue.
forkFork a run at a prior LLM turn.
importConvert a Claude Code workflow script into a draft .bot.
inspectInspect local runs, executions, events, traces, tools, artifacts, and logs.
issueManage the native kanban tracker and import forge issues.
marketplaceBrowse, submit, install, and uninstall local-registry bots/plugins.
mcpServe the operator MCP server on stdio (local + remote tool families).
memoryExport, import, and size local shared-knowledge spaces.
modelsInspect resolved model capabilities and their source.
openapiGenerate this build's OpenAPI 3.1 document offline.
openapi inventoryRegenerate the exhaustive REST inventory annex (docs/cloud-rest-api-generated.md); --check fails on drift.
pluginInstall/configure/enable/run runtime plugins.
reliabilityInspect and roll back the staged workflow-reliability rollout.
remoteAuthenticate to and drive a remote/cloud Iterion server.
reportGenerate a chronological run report.
resumeResume a paused, cancelled, or resumable failed run.
rewindRe-anchor a run on an earlier node and invalidate everything downstream (same run id).
runExecute a .bot, .botz, or bundle directory.
runnerRun the cloud NATS worker process.
runsApply local run-store lifecycle operations.
sandboxDiagnose and strictly validate sandbox configuration.
scheduleManage host-cron and keepalive schedules.
secretManage the local sealed secret store.
serverServe studio/run APIs locally or the cloud control plane.
skillManage the project/global skill library.
studioLaunch the local visual editor and run console.
superviseAttach a watcher/steering agent or install its Claude hook.
validateParse, compile, and validate without executing.
versionPrint version and commit information.

help is Cobra's generated help command.

Project and workflow commands ​

There is no project-initialisation step: iterion works against any directory. To create a bot, see iterion bots create; to attach a repo or a cloud instance, see repo scope and cloud CLI.

iterion validate ​

bash
iterion validate workflow.bot
iterion validate bundle.botz --json
iterion validate workflow.bot --exec
iterion validate workflow.bot --fixtures outputs.json
iterion validate workflow.bot --connectors
iterion validate workflow.bot --explain

--explain — the capability-drift reading. Every LLM node whose source-resolved backend is claude_code gets its tool surface printed: the native tools its tools: declaration grants on the CLI's roster, and the ones the backend withholds whatever the list says (the headless surface — a session with nobody to ask, no background shell and no later turn — and readonly:'s), with the reason per name. A reading, never a diagnostic: it does not move the verdict, and it comes from the same projection the delegate's spawn argv is built from (permissions.md). In --json, explain carries it per node.

Accepted inputs are .bot, .botz, bundle directories — and an author document, x.bot.yaml, the YAML twin a .bot can be written as (dsl.md): it is read into the program it describes — every finding the stage can attribute positioned on the document; a bundle finding carries no position, as for a .bot — and validated as its .bot would be — main.bot.yaml beside an iterion manifest or a skills/ is that bundle's main, whether or not the main.bot is written (a manifest export naming main.bot is that main); a document whose program has no written .bot form is refused (E054, the cause named — the proof behind that name is a second parse and two compiles the document pays on every validation: measured at ~25 ms on the catalogue's largest program (go test -bench ./pkg/dsl/unparse, 2026-10-27) and kept uncached, the same cost class as the stage's own work, paid by documents only; the revisit is a content-keyed cache if it ever exceeds ~1 s there), and so is one whose .bot path fmt --to bot will not write over (a directory, a file that cannot be read), in fmt's words. --json says so with source_kind: "author" and bot_path — the .bot named as the document was named, so the two paths of the result read in one form (the absolute path stays internal). A .bot already there beside the document and not the program the document describes is said as a warning on the document's first line (E056), the difference named: another program, or no program at all — a text the parser only recovers on, or one its unit load or its compile refuses on its own account. Both sides are read as a launcher reads a file — its unit, the fragments its import lines name merged in, the prompts of the bundle the two files share merged into each — and held to the compiler's own oracle, so an import line is no difference of its own, a prompt a prompts/*.md supplies is none, and a declaration's rank is none either, bar the ranks the compiled program itself reads — any ordered slice of the IR makes one — which that oracle reads for us. A comment, a blank line, an indentation or a spelling is none of it: whether the two files are byte-for-byte is what fmt --to bot --check reads, in CI, and E056 speaks of programs. Nothing is said where the program question has no subject. A side that compiles to no workflow once its imports are merged AND carries no refusal of its own is a fragment — a file under lib/, a file of prompts or schemas alone, a document carrying no workflow: — and describes no program, so the one question left about such a pair is the one about their texts, fmt --to bot --check's; a .bot refused on its own account is named for that refusal first, whatever it compiles to. And a DOCUMENT its own unit load or compile refuses describes no program for the .bot beside it to be held to: the verdict names that refusal, at its position, and the .bot is the sound side there. Which of the two is current the author alone knows, so it leaves valid as it is and the fix: line names the two --force conversions, each replacing its destination whole. Nothing is said either where fmt --to bot would write no .bot at all — a document that does not read, or one whose program has no written form (E054) — nor where no .bot is written yet. run, resume, the schedules and the MCP run tools refuse a document by name, before any effect; a directory whose only source is a document is a bundle for none of them — the twin is the main of no launch — and validate <dir> says so, naming the document. A bare main.bot whose parent is a bundle (an iterion manifest.yaml/.yml — one carrying iterion's own keys, schema_version first — or a skills/ beside it; a prompts/ alone marks nothing, and neither does a manifest of another tool, a common filename) is validated as that bundle — its prompts/*.md in scope, its manifest cross-checked — the same promotion run and resume apply, so iterion validate bots/x/main.bot and iterion validate bots/x give one verdict — including on a bundle that does not open (a manifest.yaml that does not decode), which both forms refuse by name rather than validating the bare file. A root-level sibling of that main.bot (bots/x/extend.bot) is an entry of the bundle, not a fragment — fragments live under lib/ — and is promoted the same way, opened with the file itself as the main: iterion validate bots/x/extend.bot reads the manifest and applies the bundle checks the directory form applies, and iterion run bots/x/extend.bot mirrors the skills and merges the prompts as it does for the main. Validation reports sparse DSL diagnostics in C001–C199 plus the async-interaction and structural band C240–C249, and bundle checks in C200–C234; the diagnostic catalogue is authoritative.

Every finding is printed with its source position when the stage could attribute one — file:line:column: error [C019]: …, the node's or edge's own line for a compile diagnostic, the property's own line when the finding is about one property's value (compress: bogus is reported at compress, not at the workflow w: header) — and a fix: line beneath it, the one-line remedy from the compiler's catalogue. --json carries the same findings as diagnostics, one object each: source (parse | compile | bundle), code, severity, file / line / column, end_line / end_column (the end of the positioned span, when the stage recorded one) and property (the property the finding is about, when it is about one), message, hint, node_id, edge_id. The older parse_diagnostics / compile_diagnostics / bundle_diagnostics string lists remain. The MCP local_validate tool returns this same JSON, so an agent's write → validate → fix loop reads positions and fixes, not prose.

--exec — the dry run. A program that compiles is then run twice under a simulation — every condition true and every enum at its first value, then the other way — without a model, a shell or the workspace: each node's prompts, command:, script: and postcondition: are rendered by the production renderers and every {{…}} left as written is named with the reason (a node that has not produced on this path; an {{input.X}} says which case it is — no incoming edge maps the field, unresolved_ref, a --strict failure, or another incoming edge does, unmapped_on_path, a warning worded with the edges, empty on this path only); shell text is held to bash -n (sh -n for language: sh; other interpreters are said unchecked); a human node is answered by a schema-shaped output, a wait at once, a subbot child read within the bundle's collection and simulated under the same bias (its findings prefixed node/child_node). The report — exec in --json, a block in the human output — lists each pass's status and path, the findings by node, the nodes whose output was a shape (a condition read from one decided nothing about the real bot), and the nodes and edges no pass reached. It never runs on a program with a compile error, and its findings do not decide valid: they say what the first paid run would have met. --fixtures f.json answers the named nodes with recorded outputs — {node: output}, or the replay shape, a list of {"node": …, "output": {…}} — and reports the nodes without one; it implies --exec. --strict (implies --exec) exits non-zero when the report is failing — a pass died, or a reference, shell or fixture finding stands — the switch a CI gate flips; an expression left inconclusive on a shape (below) leaves the report not clean but not failing: printed, exit 0, so a value the dry run cannot know never blocks a gate. Without --strict the exit code is the compiler's. In --json, exec.clean and exec.failing are the report's two words: a job that tolerates undecided expressions reads failing, one that does not reads clean. --exec-timeout bounds one pass, the simulated children included (default one minute); a pass that runs out of time is said so (timed_out) — the bound's doing, not the program's — and the report is not clean, since what the pass would have met past that point is unknown. --var k=v (repeatable) and --preset <name> give the dry run its launch values, with the precedence run has: a bot that guards its entry on a var — the gallery's TAG_UNSET shape — is otherwise refused at the gate on every pass, and the graph behind it is never walked; either implies --exec, and a var without a default and without a value stays shaped; a bundle's presets/<name>.md resolve as on run. Where run is lax, the dry run says: a name no var declares, or a value the var's type cannot read, is the dry run's error beside the compile verdict, attributed to its source (--var, the tool's vars, or the preset that set it); a bundle preset file the dry run cannot read is its error too, where run logs it and goes on. Each launch value is read as written, the reading a published contract takes: ${MODE:-fast} is fast, ${LIST:-a,b} the two-element string[] ["a","b"], and the shell's own environment is not consulted, so the verdict is the same on a laptop and in CI; a value that still holds a reference once its defaults resolve — ${PROJECT_DIR}/x — is handed to the dry run's engine as written, which reads it as a run does. A tool body's own environment forms take that same reading: a command:, a script: and a postcondition: go through the engine's own expansion (dsl.md) under an environment that answers nothing, so a ${NAME:-default} reads as its default — the answer the run uses when the engine's environment does not carry the name, which is why a body whose dial the engine's process DOES carry runs a value the preview did not see — and a plain ${NAME} stays as written, since whether it is answered depends on the engine's process and not on the validating one. The bash -n check therefore holds the text the engine will hand the shell, not the authored template — so a --strict gate that was green can newly fail on an unchanged .bot whose default changes the parse (echo ${SUFFIX:-|} previews as echo |): that reading is the engine's own wherever its environment does not carry the name, and the finding names the form it answered, so an engine whose process DOES carry it is the case to check by hand. The reading is reported on bodies that parse too, per body, under its own finding kind (env_read): which ${NAME:-default} forms resolved to the author's default — a run whose engine environment carries the name runs its value instead — and which plain ${NAME} forms stayed as written, with env_expansion: none named as the way to decline. It is an observation and never a defect: --strict reads past it and the verdict stays clean, because a deliberate dial read (${ITERION_DISPATCHER_PORT:-4891}) is the legitimate shape of exactly that fact. A node that declined the reading reports none, since nothing was answered. This is the recall half of C188, which judges at compile time only what it can judge without lying: a name only the engine's environment supplies is a preview fact here instead of a compile-time guess. The MCP tool takes vars (an object of strings keyed by var name, the key local_run uses), preset and exec_loop_crossings. The vars run derives from the credentials at launch (review_mode, mono_family, plan_review, llm_families) are not derived by the dry run, which reads no credential: a bot that gates on one sees a shape, or the value you give with --var. --exec-loop-crossings N bounds how often a pass crosses one bounded loop before the dry run declines its back-edge (default 3): shapes do not change from one crossing to the next, so the loop cap times its body taught the pass nothing the first few crossings did not (#1307). A loop cut short is said so on the pass (loops_cut_short), and a death that follows is the bound's doing, never the program's — clean stays the program's word, and C145's static reading of the exit is unchanged; a loop cap tighter than the bound stays the program's. A negative value removes the bound (loops run to their cap, as a run does). A key node/child_node answers a node of a simulated child; a key that names no node is a fixture finding, not a thin recording. Each simulated child's passes are listed under children — a child that dies is the parent's death — and clean says whether every pass finished (or ended at a declared fail) with no reference, shell or fixture finding and no inconclusive expression; the human block prints it as verdict. A pass that ran to a ceiling the shapes imposed — the bot's own budget:, an unbounded loop's fuel, the liveness monitor or the budget guard declining a loop edge whose exit rides a verdict the dry run shaped — is said so (ceiling) and is not a death: a loop that converges in production cannot converge on shapes, so its cap is the run's guard doing its job; pin the verdict with --fixtures to run it to its exit. Every branch of a fan-out runs to its own end under the dry run — a failure cancels no sibling — so a death in one branch beside a ceiling in another is read as the death it is. A best_effort fan-out finishes the run around its branches' deaths, but the deaths still land on the pass under dead_branches, and clean reads them: a fan-out whose branches all die is not clean, whatever the collector's await did with the finish. A bounded loop spent with no exit is the program's — C145's shape — and a death. A json-typed field has no defined shape, and the dry run does not guess one: it takes the object shape — the one-element list only where an iteration reads it (a fan_out_each over:, a foreach collection, the collection of map/filter/reduce), so the body is entered on both passes — and an expression that fails while reading a value the dry run invented (a shaped json field, a json var with neither a default nor a --var, a value computed or mapped from one, the item a fan-out drew from one) is inconclusive, never a death: the pass goes on with a shape in the expression's place, the finding (kind: inconclusive) names the value and what would decide it — --var x='<sample>', a fixture on the producing node, a typed field in its schema — and the report lists them under their own heading, inconclusive (N). A failure on the program's own value — a --var, a fixture, a literal, a field the schema types, a node the pass has not produced (a loop's previous_output before its second crossing included) — stays the death it is. The MCP tool takes exec and fixtures the same way.

--connectors — the installed catalogue. Every action: node of a program that compiles is resolved against the connector packages installed on THIS machine, through the tiers a run reads (ADR-098): the project tier <cwd>/connectors when ITERION_CONNECTOR_PROJECT_CATALOG=1 grants it, then <iterion home>/connectors. A compile checks the shape of an action id (connector.resource.verb) and nothing more — whether the operation EXISTS is the package's answer, and this is where it is asked. The report (connectors in --json, a block in the human output) names one finding per node the catalogue cannot serve, in node-id order, with the resolver's own reason: a connector no tier holds names the tiers that were consulted, so the reader knows which directory to install into; a connector that IS installed without the operation names the operation and the package version that answered. A node the catalogue serves says nothing. An install with NO catalogue at all is said once, with the roots it would have read — not as a finding per node, since the cause is one and it is the install's. Opt-in, and it has to be: what a tier serves is a property of the machine, so the default invocation reads the program alone and answers the same bytes wherever it runs; the findings are not diagnostics either — they do not decide valid and they do not move the exit code, exactly as --exec's do not. A catalog root that is there and cannot be read is the one failure that does move it: the resolution the operator asked for did not happen, so it is named (connectors_error) and the command exits non-zero, while the compile verdict stands on its own. Nothing here reads a connection record or unseals a credential — a misspelt operation id cannot cost a credential read — so what the report cannot answer is whether a BINDING exists for the node's connection:; iterion connections list is that question's surface. The MCP local_validate tool takes it as connectors, the same way it takes exec, so an agent's write → validate → fix loop reads the same report.

iterion diagram ​

bash
iterion diagram workflow.bot
iterion diagram workflow.bot --view detailed
iterion diagram workflow.bot --view full
iterion diagram x.bot.yaml              # an author document, drawn as the .bot it stands for

--detailed and --full are aliases for the corresponding --view values.

An author document (x.bot.yaml, the YAML twin of a .bot) is drawn as the .bot it stands for — the diagram iterion diagram x.bot draws once iterion fmt --to bot has written it, which diagram never does. The document is compiled as that .bot is: in the bundle the .bot's own diagram reads (a bundle's main.bot, or the bundle any other workflow file belongs to — a manifest export naming the .bot is that .bot, written or not, spelled through a symlinked directory of the bundle or not), with the fragments its imports name read where that diagram reads them, and through the same stages. What refuses the .bot refuses the document, in the same words: the bundle's (a manifest that does not decode), the unit's errors, the compile's, the MCP servers' (a server nobody declares, a .mcp.json that does not parse). Before those, what only a document has refuses it: its own errors, a program with no written .bot form (E054 — the same measured proof validate pays and keeps, go test -bench ./pkg/dsl/unparse), and a .bot path fmt --to bot will not write over (a directory, a file that cannot be read). --json then carries source_kind: "author" and bot_path, as validate does — the .bot named as the document was named, in the form file carries. A .bot beside the document that is not the program it describes is validate's word (E056), not the diagram's: a diagram carries no diagnostic.

iterion fmt ​

bash
iterion fmt workflow.bot                 # rewrite the file in its canonical form
iterion fmt bots/my-bot                  # every .bot under a directory, lib/ fragments included
iterion fmt --check bots/                # CI: exit non-zero when a file would change or is refused
iterion fmt x.bot.yaml                   # an author document, in its canonical YAML form (its YAML comments kept)
iterion fmt --to bot x.bot.yaml          # write the .bot the document stands for, beside it
iterion fmt --to yaml x.bot              # write the author document of a .bot, beside it

fmt parses each file, writes it back through the writer the studio saves with (pkg/dsl/unparse) and proves, before writing, that the text reads as the same program (Verify: same parse, same profile, same compiled workflow and diagnostics, prompt bodies in their canonical form). The text lands on the file's own bytes: a BOM and CRLF line endings are kept. Verify also compares the comments: the text carries the same ones, on the same declarations. A value holding a newline is written over lines of its own in every profile (#1612): a | block scalar at a property line when the reader gives the value back exactly (ends with a newline, no leading blank line, no whitespace-only line, no later line de-denting below the first), a backtick raw string otherwise — and one blank line directly after a block scalar's body is never written, since the reader would absorb it into the value. A file it cannot rewrite without changing it is refused by name and left as it is — one that does not parse, one whose comments the round trip would lose, one holding a value NO multi-line form carries (a carriage return, or a backtick beside a newline with no trailing one): one such value folds the whole file back to the one-line escapes, all or nothing, so a value its author wrote over lines would come back as a single line — the same program and not the same file — while the files beside it are formatted all the same. That last refusal is not fmt's alone: it lives in pkg/dsl/canon and every path that turns a document into the bytes of a .bot asks for it — the studio's save of a file, of one file of a unit, and the cloud write-back — so a file fmt leaves alone is a file the studio refuses to rewrite too, naming it and the line the value is written on. The studio's Source view still SHOWS such a file: as its own text, with the reason, read-only. Folding is a before/after property, so the refusal is only made where the file's current text is in scope; the two writes a client performs — PUT /api/teams/{id}/bot-sources/{slug}/files/{path} and the whole-bundle PUT /api/teams/{id}/bot-sources/{slug} a cloud save of a bot in several files lands on — are each judged on the two texts their route holds. Those two take the author's OWN bytes, so their refusal keeps an explicit escape hatch (#1679) where the writer-rendered paths keep none: allow_fold on the request body, off by default — a write that names it lands, and each value it collapsed is named (file, line, the line length it now has) in the response's warnings; one that does not is still refused, the refusal naming the hatch. iterion remote admin bots push spells it --allow-fold. A fold the reader cannot TELL — a stored file past the read budget — stays refused either way: no hatch asks a write to proceed on a question nobody could answer. A comment goes back where it was written, its own indentation and its paragraph breaks included; one whose line the writer does not emit (a property equal to its default, an empty list) is written at the end of the block it was in rather than dropped. Two move, by name: a comment at the END of the dsl: header's own line, and one above the file's first declaration separated from it by a blank line — both land at the file's head, which is where the head of a .bot has always been read. .botz archives are not workflow files: naming one is an error, a walk passes over them. --check writes nothing and exits non-zero when a file would change or is refused; --json lists each file (changed, written), the refusals and their paths (refused_paths).

Author documents. fmt takes a .bot.yaml — the YAML twin of a .bot (dsl.md) — like a .bot: named or met in a walk, it is rewritten in its canonical YAML form (pkg/dsl/canon, Document), the program it describes written back by the author writer and proven to read as the same program, on the document's own bytes — a BOM and CRLF line endings are kept. Its YAML comments ride the nodes they hang on — a head comment above its key, a line comment after its value, a foot comment below its block — wherever the canonical form puts that node. A document is refused and left as it is, named among the refusals, when the rewrite would lose what its author wrote: a comment the written form cannot hold where it was written (one right after a flow collection's opening bracket, which YAML drops; or one that would move on the next pass), and a document the .bot reads otherwise than it was written — a prompt body with leading or trailing blank lines the lexer settles, a line separator in a block scalar, what validate reports as E053 — since the writer would put that reading in the author's place. A # that ends a plain text value (E053 too) is the exception: the comment stays on the value, so the text — and the warning — stay the author's. A # OPENING a line of a multi-line value — a block scalar's body, a quoted value written over lines up to its closing quote — is the author's text and no comment: the document's last content line is read below it, so such a document is rewritten rather than refused over prose it wrote. The lines written BELOW the document's last content line that no node carries are kept as the author wrote them: the canonical form re-emits the explicit markers the document had — a --- opening it, a ... closing it — with the trailing region they sit in, so the comment on a ... marker's own line, a block above it, a block below it, a block a blank line keeps off the document's foot, a second block, all come back where they were written, and a document already in that form is handed back byte for byte. One line has no place in a written form: a %YAML or %TAG directive, which opens a stream rather than describing the program — a document carrying one is refused, the directive named, with a comment written on its line. fix and dsl migrate take no document: named one, they say which command writes the .bot they work on. --to bot writes the .bot a named document stands for, beside it (x.bot.yaml → x.bot), after the proof validate reports as E054 when it fails — a document whose program has no written .bot form is refused (the proof is the measured one validate keeps uncached, go test -bench ./pkg/dsl/unparse); the .bot written carries the settled reading of an E053 body, and the reading is said under note:, as are the document's YAML comments, which the .bot is not written with — counted, the first named (a # inside a plain value starts one); its frontmatter is spelled as the writer spells a catalog: (a list as [a, b]), whatever the original .bot wrote — the identity is kept, the spelling is a comment's. A document whose .bot name no surface reads back no longer exists as a refusal class: the workflow file suffix is case-folded (one rule with bundle.Detect, #1762), so UPPER.BOT.YAML converts to UPPER.BOT and fmt, the walks and --to yaml all read it. --to yaml writes the document of a named .bot (x.bot → x.bot.yaml) from a text that parses without an error — never from one the parser recovered on, even with --force — once it has proven the document reads back as the same program (a writer that spelled what the reader refuses is refused, nothing written). Its catalog: is what the catalog reader reads off the frontmatter — one block and one reading, shared by the bundle loader, the author writer and this notice: parser.Frontmatter takes the fence block that opens the file's head (blank lines, the dsl: header and import lines may precede the fence, a comment may not; a BOM or CRLF endings read as the lexer reads them), and workflowfile.DecodeFrontmatter decodes it, so a key named twice keeps its last value in both, and a block that reading does not read (a scalar triggers:, a malformed line) gives the .bot no identity and the document no catalog:. What the document does not carry is said under note:, off the document written and with the reason: a catalog: it has none of, the frontmatter keys beyond the four of catalog:, the ordinary comments — the lines of a block that became no catalog: counted among them. Either way a destination already there and not what the source writes is refused without --force, one that is already it is a no-op, one that is there and is not a file (a directory) or cannot be read is refused, whatever --force says, while the files named beside it are converted all the same, and --check reports what a write would do — the refusal included: a differing destination is refused under --check as it is by the write, and "would write" only with --force — the CI form of "is the .bot beside the document the one it writes?". --to takes named files: a conversion writes beside what it reads, and a walk would write trees. --json carries each conversion as a file with from, and the notices as notices.

--baseline <file> makes --check usable as a gate on a tree that is not yet wholly canonical. The file lists, one path per line (# comments ignored), what this tree already knows its canonical form refuses — in this repository, .fmt-refused, which stands EMPTY since #1612 gave the writer its multi-line forms: what a value no form carries (a carriage return, a backtick beside a newline with no trailing one) still lands there. The check is then GREEN while the refusals are exactly those, and red — naming the file — on each of the three ways that stops being true: a formattable file drifted out of its canonical form, a file newly became unformattable, or the baseline names one nothing refuses any more (a file formatted at last, renamed, or deleted). That last direction is the ratchet: the backlog cannot grow quietly, and it cannot be left behind once the writer can carry those files. A baseline that is not there is an error, never "nothing is refused". task fmt:check runs exactly this over bots and examples, and the fmt-check CI job runs the task's command.

iterion fix ​

bash
iterion fix workflow.bot                 # apply the mechanical remedies, in place
iterion fix --dry-run bots/my-bot        # list every edit, write nothing

fix applies the remedies that are the same every time — today C137: a {{ref}} an author quoted in a tool's command: or postcondition: loses exactly the quotes around it (the runtime shell-quotes a ref already; the two quotings cancel) — on the file's own bytes, comments and layout untouched, and proves each result before writing it: the text parses, and compiles to the same diagnostics minus the fixed. Quotes that hold more than the reference ('v={{vars.x}}') are not mechanical and are left to the author, said so under left, as is every other diagnostic — and so is a tool inside a group, whose literal serves every use (not rewritten mechanically yet). A file is fixed within its unit: naming a bot's main fixes its lib/ fragments too, naming a fragment reads it through the main that imports it. A file that does not parse is refused by name while the others are fixed. validate --json carries the same edit on the diagnostic it remedies (edit: code, line, column, from, to), so an agent's loop can apply it without a second command.

iterion bundle ​

bash
iterion bundle pack my-bot
iterion bundle pack my-bot --output dist/my-bot.botz --force

bundle only packages. Create the source directory with iterion bots create. An author document beside the sources (*.bot.yaml, the YAML twin of a .bot) is a draft, never a member of the bundle: pack leaves it out and says how many under Drafts left out when there are any; the --json counter drafts_left_out is always present, so zero reads as zero. See bundles for the archive contract.

iterion import ​

bash
iterion import .claude/workflows/review.js
iterion import review.js --name review --out review.bot
iterion import review.js --dry-run

Import never executes JavaScript. Recognised agent/phase/loop/routing shapes become DSL; unknown constructs become ## IMPORT markers. The result is a compile-checked draft and the conversion is intentionally lossy. See import.

iterion dsl ​

bash
iterion dsl spec                                  # the property registry, rendered
iterion dsl spec --write                          # regenerate the committed reference and skill sections
iterion dsl migrate --to 2 bots/my-bot            # move a bundle to syntax profile 2
iterion dsl migrate --to 2 --dry-run --show-prompts x.bot
iterion dsl migrate --check bots/                 # CI: exit non-zero when a file would change

dsl spec renders the property registry every kind of the language is described by, or regenerates the committed documents from it (task dsl:gen; task dsl:check fails on a stale rendering). dsl migrate moves .bot files to a newer syntax profile (docs/dsl.md § The syntax profile) by the edits that change meaning between profiles and nothing else — the dsl: 2 header in, the profile-1 strict-escape directive out, every quoted literal holding a backslash re-spelled from its profile-1 value — and proves the result before writing it: both texts parse, they read as the same document, the file's ## --- frontmatter is unchanged. Named prompts that keep paragraph breaks they used to lose are reported (--show-prompts) and refused on --strict-prompts; project_root: has no profile-2 form and is refused with the remedy. A bundle's manifest gets requires.iterion raised to this build's version — the one that reads the profile — so an older runner refuses it at admission (--floor overrides the version; a dev build raises nothing and says what to declare).

Run lifecycle ​

iterion run ​

bash
iterion run <file.bot|file.botz|bundle-dir> [flags]

Inputs and execution:

FlagMeaning
--var key=valueSet a variable; repeatable.
--preset <name>Apply an in-source preset before --var.
--recipe <file>Apply a recipe JSON overlay.
--run-id <id>Supply the run id.
--store-dir <dir>Local store override. Without it, reuse a managed project .iterion or use the deterministic project slot under $ITERION_HOME/projects/ (normally ~/.iterion/projects/).
--timeout <duration>Outer run deadline.
--log-level error|warn|info|debug|traceLogging verbosity.
--no-interactiveReturn at a human pause instead of prompting on the TTY.
--skip-mcp-healthWarn instead of aborting when a declared MCP server fails startup health.
--auto-resume <n>Retry eligible failed_resumable causes with capped backoff.

Launch-time graph overrides:

FlagMeaning
--model selector=modelOverride by node id, id glob, or kind (agent/judge); repeatable. A bare model targets all LLM nodes.
--backend selector=backendSame selector rules for a supported backend; repeatable. claw/claude_code are in the default auto-selection order; Codex, pi, Kimi and Grok are explicit opt-ins.
--fallback <backend>:<model>Run-level fallback route taken when an agent node's primary fails, e.g. claw:openai/gpt-5.5. Applies only to agent nodes that declare no fallbacks: of their own, never to judges, and uses the default trigger set (usage_window, unavailable) — author a fallbacks: block for anything finer. See ADR-087.
--max-cost-usd, --max-duration, --max-tokens, --max-iterations, --max-parallel-branchesOverride non-zero workflow budget fields.
--loop-budget-guard on|offDecline a loop back-edge the remaining budget cannot fund, so the run leaves through its own exit path with the work it banked instead of dying mid-iteration. Empty inherits the loop clause guard: then the workflow loop_budget_guard: then ITERION_LOOP_BUDGET_GUARD; default on.
--review-mode mono|dual|autoSelect the reviewer topology for workflows that declare a review_mode var (currently review-pr and evolve). mono runs one family, dual runs both, and auto resolves to mono on the preferred detected family whose slot has a servable route — a backed family with none refuses the launch (#1221), and dual only where no participating family is detected at all, which deletes an explicit mono_family pin with it. No-op for other workflows.

Access/isolation:

FlagMeaning
--permission off|ask|denyOverride the tool-permission gate.
--permission-allow, --permission-ask, --permission-denyAdd repeatable Claude-Code-style rules.
--sandbox none|autoForce sandbox mode or inherit when omitted.
--sandbox-default-image <ref>Fallback image for auto.
--sandbox-host-state auto|noneBind or exclude host ~/.iterion/~/.claude; use none on multitenant runners.
--compress off|on|ultraOverride command-output rewriting/compression.
--auto-memory on|offLet agent/judge nodes read and maintain a persistent MEMORY.md across runs of this bot on this project. Empty inherits the workflow/node auto_memory: then ITERION_AUTO_MEMORY; the default is off, so a run is hermetic unless it opts in. Honoured by claude_code, claw and pi.
--repo-devbox on|offInstall the target repository's devbox.json for this run. Turn it off for a run that reads a repo without building it (a review, an audit) and would otherwise pay its whole Nix toolchain; the bot's own devbox.json is unaffected. Empty inherits the workflow repo_devbox: then ITERION_REPO_DEVBOX; default on.
--supervisors on|offSpawn the workflow's DSL-declared supervisor watchers. off runs unsupervised — cost control, or isolating a suspect steering policy. Empty inherits ITERION_SUPERVISORS; default on. A skip is always logged.

Worktree finalization:

FlagMeaning
--branch-name <name>Override the run branch name.
--merge-into current|none|<branch>Select final target or keep only the run branch.
--merge-strategy squash|mergeCollapse run commits or preserve fast-forward history.
--auto-merge=<bool>Apply the finalization automatically; CLI default is true, studio launches defer it.

See permissions, sandbox, merge policy, memory, and settings precedence.

iterion inspect ​

bash
iterion inspect
iterion inspect --run-id RUN --events
iterion inspect --run-id RUN --list-nodes
iterion inspect --run-id RUN --node analyze --section trace
iterion inspect --run-id RUN --exec exec:main:analyze:0

Run-level mode takes --events and --full (show all details). Node selection supports --branch, --iteration (-1 latest), --section summary|events|trace|tools|artifacts|interactions|log|all, and --log-tail.

iterion report ​

bash
iterion report --run-id RUN
iterion report --run-id RUN --output report.md

The report reconstructs summary, artifacts, timeline, routing, branch lifecycle, interactions, and budget events.

iterion resume ​

bash
iterion resume --run-id RUN --answer approved=true
iterion resume --run-id RUN --answers-file answers.json
iterion resume --run-id RUN --answer music=@./theme.mp3   # file field → staged as an attachment

--file defaults to the persisted source path. --force ignores source drift; --force-stale takes over a running run whose event stream has been silent for at least 60 seconds. Resume also accepts --auto-resume, model/backend overrides, --fallback, all --max-* budget overrides, permission mode/rules, and the four run-shape toggles --auto-memory, --repo-devbox, --loop-budget-guard and --supervisors. None of these launch overrides are persisted on the run, so repeat them when continuity matters. See resume.

iterion fork ​

bash
iterion fork --run-id PARENT --node implement
iterion fork --run-id PARENT --node implement --turn 0 --new-inputs inputs.json

The new run is created in cancelled state at the selected conversation turn; resume it to execute. --rewind-code additionally requests the captured code snapshot where available. --name controls the friendly name.

--new-inputs crosses the same var-constraint gate a launch crosses — a value outside a var's [enum: …] or off its [matching: "<re>"] is refused here, naming the var, the value and the constraint. Only the values you change are judged: re-sending the parent's own values (what the studio's ForkDialog does when you edit one field) never re-opens a declaration that was tightened after the parent ran. A value that reads from the environment (${VAR}) is refused on a constrained var rather than guessed, because the process that runs the child is not the one that reads it here.

Whatever this pre-check admits, the child re-judges once: the values you supplied are recorded on the child (fork_supplied_inputs), and its first resume judges exactly those keys against the var constraints of the workflow it compiles — refusing the child, typed, if one violates. A declaration tightened after that first resume never strands it, and a child with nothing recorded is never re-judged at all — the same invariant that keeps resume off stored values, one gate later.

The pre-check needs the source the parent executed, which runs record on their own document since 2026-08-04. A fork of a run older than that — or of one whose source was dropped (over the 1 MiB record cap, or cleared by a forced cloud resume) or no longer compiles — cannot be pre-checked: the fork still goes through, prints a note saying the values were admitted without one, and the child's first resume does the judging (a plain recovery fork without --new-inputs never meets any of this). A value the pre-check can already refuse — the enum, pattern and environment cases above — is still refused at the keyboard rather than at the child's launch. (POST /api/runs/{id}/fork answers 400 for those refusals and carries the note in its response; the CLI prints both.)

iterion rewind ​

bash
iterion rewind --run-id RUN --auto           # locate the edit itself
iterion rewind --run-id RUN --node implement # or name the pivot
iterion resume --run-id RUN --force          # after editing the .bot

Moves an existing run's checkpoint back onto an already-executed node and invalidates the outputs downstream of it, so the next resume replays from there. Same run id — use fork when you want the original left intact. --auto diffs your edited .bot against the source the run executed and rewinds to the earliest affected node — the bot-development loop in one step. --node accepts any node with a recorded output (including tool and compute, unlike fork's turn anchor); --file overrides the source the graph is read from. Budget accounting, loop counters, and events.jsonl are preserved; artifacts the dropped nodes published get a superseding rewound marker version.

A successful rewind leaves the run in paused_operator at that checkpoint. It does not execute anything until an explicit resume; stale cloud launch deliveries are ignored while the rewind guard is present.

The workspace is restored too, on BOTH run shapes — a worktree: auto run through git, an in-place run through workspace versioning. On an in-place run that workspace is your live checkout, so --restore-scope bounds what comes back:

valuerestoresdefault for
producedonly paths this run recorded changing after the pivot startedin-place runs
fullevery versioned path in the snapshotworktree: auto runs
nonenothing; the node replays against the tree as it stands— (--keep-files is the old spelling)

produced is refused on a worktree: auto run — git reverts the whole tree or none of it — rather than silently widened. It is the in-place default because a rewind is launched right after you edit files — --auto derives the pivot from that edit — so putting the whole tree back would revert your own work along with the run's. What iterion cannot attribute it reports rather than guesses: paths it overwrote that had changed since the run last recorded its workspace, and paths it left in place for the same reason (which may be a failed node's partial output, or your editor). The pre-rewind state is banked first either way — --list-snapshots / --restore-snapshot is the way back, and it is deliberately full-tree. See resume.

iterion runs prune ​

bash
iterion runs prune --dry-run
iterion runs prune --older-than 168h --keep-last 100

Default retention deletes finished, failed, and cancelled runs older than 720 hours. failed_resumable requires explicit inclusion via --status. Only <store-dir>/runs/ is touched; worktrees are not removed — use iterion clean for those.

iterion clean ​

bash
iterion clean                             # dry run, conservative, this project
iterion clean --apply
iterion clean --all-projects --older-than 720h --apply
iterion clean --level moderate --keep-last 10 --apply

Reclaims the per-run worktrees under <store-dir>/worktrees/. A worktree: auto run that succeeds promotes its commits onto a branch and removes its checkout; a failed or interrupted one deliberately leaves the checkout behind for inspection and never comes back for it. On a long-lived store that leftover pool is where the disk goes, and runs prune cannot reach it.

The runtime also keeps that pool bounded on its own, reclaiming the oldest entries nothing is lost by deleting each time a run creates a worktree, and warning when it cannot — see worktree-pool.md. It is a floor, not a replacement: everything dirty, resumable or unlanded is left for this command, which is where the reclaimable space usually is.

What decides safety is not age and not run status alone, but what git can prove about the commits:

landingmeaningtaken at
mergeda ref whose tip is not this HEAD contains this HEAD — another line of work was built on topconservative (clean tree), moderate (dirty)
own-branchrefs contain HEAD but every one points exactly at it: labels keeping the commits alive, nothing built upon themaggressive
orphangit cannot account for the directory at allaggressive
unlandedno ref contains HEAD, or git could not answernever
nested-repothe tree holds a repository of its ownnever

Classifying by what the refs were built upon rather than by ref name matters in practice: iterion creates run worktrees detached (git worktree add <path> <sha>), so there is no symbolic ref to compare a branch name against, and a run whose commits were merely promoted to iterion/run/<x> has not been adopted by anything yet.

Refs under refs/iterion/ — the per-run checkpoints iterion writes itself — are consulted, because they do hold a run's commits alive; but they are that run's own bookkeeping and are reaped with it, so containment by one of them can never mean merged. Annotated tags are compared on their peeled commit: %(objectname) is the tag object's id and never equals the commit, so a release tag sitting on a worktree's HEAD would otherwise read as work built on top of it.

Three guards no level lifts:

  1. A run that is not terminal keeps its worktree. Checked against run status, never mtime — a run can spend hours inside one agent turn without touching its checkout, so age alone would call it abandoned. A run whose run.json cannot be read is treated as active, not as absent. The sweep takes the same per-run lock iterion run and iterion resume hold for a run's lifetime and keeps it across the deletion: the window a status re-read alone leaves open is not an instant but the whole removal, which on a real worktree runs for seconds.
  2. An unlanded worktree is never deleted. Its commits would survive only in the reflog, which expires; recovering or discarding that work is a decision for the operator and for git, not for a sweep.
  3. A worktree holding a repository of its own is never deleted — an initialised submodule, or a plain clone dropped inside it (a vendored checkout, a dependency's source kept beside the code that uses it). Its objects live under the directory, so containment in the outer repository proves nothing about them, and being gitignored the tree still reads clean. A submodule merely declared and never initialised does not trigger it: git worktree add never populates submodules, so that is their normal state and there is nothing there to lose.

Git itself must be usable before any verdict is formed: a git missing from a cron PATH, or a malformed config, would otherwise make every directory unclassifiable at once. A git that answers but fails on a particular directory yields unlanded, never orphan. Only two things make a directory read as "not a worktree": git saying so itself, and git answering for some enclosing repository instead of that directory.

What iterion mirrors into a run worktree at run start does not count as uncommitted work — it is written by iterion, not produced by the run. That is untracked content under .claude/skills/, .claude/commands/, .claude/agents/, plus .claude/settings.json exactly — and, tracked or not, the mirror's own bookkeeping: .claude/.iterion-managed/ and an .iterion-managed/ directly inside each of those three directories. The engine is more precise than the sweep about the rest: its mirror records every file it writes, with the content hash it wrote, in .claude/.iterion-managed/mirror-manifest.json, so a tracked file under .claude/ is mirror noise only while its bytes still hash to what the manifest recorded (a deletion made by the mirror's own pruner reads the same way), and an edit by the run — or by anyone after the mirror — is the run's work. The sweep deliberately keeps the coarser reading and treats any tracked change under those directories as work, even the mirror's own refresh: a wrong "noise" verdict would delete a worktree, a wrong "work" verdict only keeps one longer (on a repository that versions the mirror's output, that is what --level moderate is for). .claude/settings.json.orig is a failed merge, not a mirror; and a .claude/ nested deeper in the tree is the run's own scaffolding of a sub-project.

Immediately before a deletion the whole verdict is derived again, because the classification is a photograph and a sweep runs for tens of seconds. A worktree whose HEAD moved, which gained a repository of its own, or whose tree turned dirty in a way the current level no longer admits is spared — at conservative a tree that turned dirty is; at moderate and above a dirty merged tree remains eligible, by that level's own contract. Re-asking only about the working tree would not do: a commit leaves a clean tree.

A worktree a concurrent sweep already removed is reported already-gone, neither deleted nor judged.

Every git answer is refused unless git is talking about that directory. Asked about a directory merely nested inside a repository — and the project-local <repo>/.iterion/ store puts the whole worktree pool inside the operator's checkout — git walks up and answers for the enclosing repository, whose clean status and refs would read as a landed worktree.

Dot-prefixed entries such as worktrees/.state are left alone: they hold gate state shared across runs, not one run's checkout. Gitignored content, by contrast, is deleted at every level — in a run worktree it is the build output the command exists to reclaim — and the count of gitignored paths is reported per worktree so it is visible before --apply.

The command is a dry run until --apply, and reports what it spared and why, so "nothing was eligible" is never confused with "everything was guarded". Each spared entry carries a skip_reason: run-active, unlanded, nested-repo, too-recent, keep-last, already-gone, paused-run (a dormant run waiting on operator input), or needs-higher-level, or resumable. run-active also covers a run whose lock another iterion process holds; the reason is printed beside the entry.

iterion resume restarts a failed_resumable or cancelled run in its existing worktree, and both report as terminal to a poller — so the guard above lets them through, and sweeping one destroys the resume while every other check nods along, since the commits are merged and nothing is lost but the ability to continue. Those worktrees are spared as resumable, with their size, and --include-resumable is how to say the resume is not wanted. It is the same opt-in runs prune requires before it touches the same statuses. On a long-lived store they are most of what accumulates, so that line is usually where the reclaimable space is. Sizes are measured for the candidates, for what only the level ladder holds back — so the yield of the next level up is visible before choosing it — for keep-last and for resumable; not for run-active, unlanded, nested-repo, too-recent or already-gone, which report 0 — the last because another sweep has already reclaimed them: measuring costs a walk of every file, and the live checkout of a running campaign is never walked at all.

--older-than defaults to 168h (7 days) and is compared against the mtime of the worktree's root directory, which changes when an entry is created, removed or renamed directly in it — not when a file deep inside is edited.

A failed deletion does not abort the sweep: the rest is still processed and the report still printed, since an aborted sweep strands what it already deleted with no record of it. Failures are listed under failed in --json, apart from deleted, so deleted_count counts deletions rather than attempts — in a dry run it counts what would be deleted, which dry_run: true and each entry's "deleted": false make explicit.

After each successful deletion the worktree's registration is dropped from the parent repository — where there is one to find: an orphan has no repository git will name. git worktree prune is deliberately not used — it sweeps the whole repository and would also drop the registration of a worktree merely absent at that instant, such as an operator's checkout on an unmounted volume, discarding its index and staged work.

--keep-last applies per store, so under --all-projects it keeps N of each project's worktrees rather than N across the whole machine. Run records are kept unless --with-runs is passed — they are the journal of what the agent did, and they are small.

iterion runs questions / iterion runs answer ​

bash
iterion runs questions <run-id>
iterion runs answer <run-id> <interaction-id> "<answer>"

Inspect and answer the non-blocking questions an agent posts with the ask_user_async tool (interaction: async, ADR-081 — see async-interaction.md). runs questions lists the still-unanswered questions of a run; runs answer records one answer and queues it for delivery to the asking node's inbox — the running agent picks it up at its next turn boundary and the run never has to pause. Both take --store-dir (default .iterion). For a run paused on a blocking question, use iterion resume --answer instead.

Bot creation, discovery, and extension distribution ​

iterion bots ​

bash
iterion bots create <slug> [--template <id>] [--workdir <dir>] [--dest bots]
iterion bots templates
iterion bots list
iterion bots list --paths bots --paths examples --format markdown
iterion bots list --category verify --tag security --format tree
iterion bots install <git-url|path> [--path <bundle>] [--ref <git-ref>] [--name <id>] [--dest <dir>] [--force] [--replace-unrecorded]
iterion bots sync [--workdir <dir>]
iterion bots update <name> [--ref <git-ref>] [--workdir <dir>] [--allow-dirty]
iterion bots regen-catalog

bots create scaffolds a bot bundle under bots/<slug> — main.bot, manifest.yaml, README.md, .gitignore, and the skills/ prompts/ attachments/ presets/ layout — then refreshes the generated catalogue. It is the CLI half of the studio builder at /bots/new: both render through pkg/botscaffold, so a bot created either way is identical. The generated workflow is parsed and compiled before anything is written.

The name must be free everywhere discovery looks (bots/, examples/, .botz/), not merely under --dest: a duplicate name makes catalogue routing ambiguous. A collision exits 2 and names the conflicting bot's path. A slug is kebab-case (^[a-z][a-z0-9-]{1,63}$); the rendered workflow name is the slug with - turned into _, since a DSL identifier takes no hyphen — bots create release-readiness declares workflow release_readiness:.

Staging and the sweep. A create renders into a staging directory beside its destination before anything is published: a dot-directory named .<slug>.scaffold-<random>. A create killed before its publish leaves the staging standing — nothing of it is a bot, and discovery never lists or routes it — and every server boot sweeps the stale ones: over each discovery root, any staging-shaped directory older than one hour that carries the scaffold's marker file .iterion-scaffold-staging is removed with its contents. That marker name is reserved: it is the scaffold's provenance, not a name the scaffold happens to like — a directory beside a discovery root that matches the staging shape and carries a regular file by that name is sweep-eligible, so a cp -r of a dead staging, a backup, or a user dot-directory given a file by that name loses its contents at the first boot past the hour. A server without a configured work directory skips the sweep (bots: staging sweep skipped: no work directory configured in its log). Stagings written by a binary older than the marker carry none and are never swept: an operator removes one by hand — the directory holds no bot, nothing routable, so after checking nothing of yours is inside, delete it.

FlagMeaning
--template <id>Start from a gallery template (default blank); iterion bots templates lists them. Five render the single-agent workflow with a different mission (blank, daily-digest, code-reviewer, docs-writer, issue-triager); ten render a complete, commented shape — campaign-loop, review-fanout, plan-gate-implement, scheduled-digest, per-ticket-subbots, verified-action, async-questions, multi-file, library (a bot in several files: main.bot imports lib/schemas.bot and lib/nodes.bot), contract (a bot whose public contract — inputs mirroring its vars, outputs naming their producers, a delivered file, criteria, effects — is bound to its program) — each held to its form by a test (the table in SKILL.md § Start from a template). A shape's annex files (a child worker.bot, prompts/*.md, skills/*.md) are written with the bundle. campaign-loop and scheduled-digest need jq and ship a devbox.json + devbox.lock pinning it, which the engine installs on any image that ships devbox (every iterion sandbox image does, and ships jq too).
--workdir <dir>Workspace root anchoring --dest and the catalogue refresh (default: cwd).
--dest <dir>Parent directory for the bundle, resolved against --workdir (default bots).
--display-name, --description, --instructionsPre-fill catalogue metadata and the agent's mission.
--model, --backendPin instead of auto-detection.
--worktree, --sandboxIsolation dials; only override the template when passed explicitly, and every template honours them. The worktree dial is on by default for the templates that commit — blank, docs-writer, campaign-loop, plan-gate-implement, verified-action (--worktree=false opts out, writing worktree: none) — and off for the ones whose deliverable is a file in the checkout, a read of pending changes, or a board write.

bots list scans bots and examples by default and emits json, markdown, a generated skill, or tree — the navigation spine (category → bot → presets). --category <slug> keeps bots in the named categories (build, verify, harden, document, operate, steer; the pseudo-slug uncategorized selects the Uncategorized group — bots with no category OR an unknown slug, exactly the bots the grouped views file there; repeatable, OR within the flag); --tag <t> keeps bots carrying EVERY listed tag (repeatable, AND across tags). Installs default to the git-ignored workspace .botz/ and never run the bot — pass --dest bots to install into a committable location. regen-catalog rebuilds Nexie's generated bot catalogue from manifests and .iterion/bot-overrides.yaml. create, install, sync and update refresh that catalogue too, and a refresh that fails does not fail the gesture: the command prints ! catalog not refreshed: with the cause and the regen-catalog retry (--json carries it as catalog_error). Install names are bot names — letters, digits and . _ -, starting with a letter or digit (botlock's own charset) — so a pulled bundle's manifest cannot write a line of the output. A bots sync that aborts after materializing some dependencies leaves ONE --json document, {"results": […], "error": …}, and exits non-zero — never a second document after the first.

--force replaces an install, not authored work. A --force install removes the target directory whole before copying the source over it, and --dest bots is a committable location that holds hand-written bundles — so --force is held to what the flag says it overwrites: an existing install. The record is the installer's own, an origin file it writes beside everything it installs (<dest>/.origins/<name>.json, outside the bundle so it does not alter the content hash). A target with no readable origin record beside it is refused, naming the directory and the escape hatch; --replace-unrecorded is that hatch and replaces it anyway. An origin record that cannot be READ is refused the same way: an unreadable ledger is not a licence to delete, since whether the target is authored work is then unknown. bots sync and bots update are not subject to it — bots.lock is the record of management for .botz/<name>, and making .botz match that pin is their whole contract.

bots sync materializes every dependency pinned by the project-root bots.lock into .botz/ and rejects content whose bundle hash differs from the lock. bots update <name> fetches that dependency again, computes its content hash, atomically rewrites the pin, and syncs only that bundle. A local Git source records its current commit and must be clean; --allow-dirty is an explicit, non-reproducible development escape hatch. Commit local source changes before updating whenever the pin is meant to be shared.

iterion marketplace ​

bash
iterion marketplace list [--kind bot|plugin] [--query text] [--tag tag]
iterion marketplace submit <git-url|path> [--path subdir] [--ref ref]
iterion marketplace install <slug> [--force] [--replace-unrecorded]
iterion marketplace uninstall <slug>

The registry lives under <store-dir>/marketplace/. Submission validates/indexes metadata but does not install. Bot installs land in workspace .botz/; plugin installs land under ~/.iterion/plugins/; neither auto-runs. A bot install or uninstall also refreshes Nexie's catalogue, and a refresh that fails does not fail the gesture: the command prints ! catalog not refreshed: with the cause and the iterion bots regen-catalog retry (--json carries it as catalog_error).

iterion plugin ​

Subcommands are list, info, install, uninstall, enable, disable, config, and run.

bash
iterion plugin list
iterion plugin enable repo-falcon
iterion plugin config firecrawl --set api_url=http://localhost:3002
iterion plugin run repo-falcon index
iterion plugin install <directory|git-url>

Built-ins are rtk (enabled by default), graphify, repo-falcon, codeindex, and firecrawl (disabled by default). Third-party installs are disabled until enabled. A bare public skill library can be installed through the same path. See plugins.

iterion skill ​

Subcommands are add, export, import, list, rm, and show. --project targets the project store; otherwise the global store is used.

bash
iterion skill add changelog-writer --from skill.md
iterion skill import https://github.com/acme/skills
iterion skill list

See skills library.

iterion models and iterion openapi ​

bash
iterion models
iterion models openai/gpt-5.5 --json
iterion models --refresh
iterion openapi --output openapi.json

Model data reports the online-cache or curated-fallback source. openapi is offline and code-generated; use iterion remote openapi for a server's live spec.

In the iterion models table the REASON, TOOLS and TEMP columns read yes, no or ? — ? when no table answered the flag, which is a gateway id (openai_compatible/<id>) whose catalog record states none; --json then leaves reasoning, tool_call and temperature out of the entry rather than printing a false nobody stated. A vendor id always carries all three.

iterion models pricing audits the rate a run is actually charged at against the price published by the spec aggregator (models.dev), and reports every disagreement — it never rewrites the committed table in pkg/backend/cost/cost.go (prices feed budget decisions, so a change is a human judgement call). The charged rate is resolved in tiers: claw's live registry, then the aggregator's published pair, then the committed table. Three verdicts: DISAGREES (the charged rate differs from the published one — in practice claw's live registry quoting something else, since the aggregator's own pair is used when claw has no entry), IGNORED (a price is published and a run still reports none — now expected only for a half-published pair, which is refused whole rather than priced with a zero half), and table only (nothing is published — expected for brand-new models, which is why the committed table exists). --refresh refetches published prices first; --check exits non-zero on drift, for CI:

bash
iterion models pricing                  # audit against the cached specs
iterion models pricing --refresh        # refetch first, then audit
iterion models pricing --check          # non-zero exit on drift, for CI

Local services and automation ​

iterion studio ​

bash
iterion studio --dir . --port 4891
iterion studio --bots-path ./bots --no-browser
iterion studio --workspace --port 4891

The listener defaults to loopback. --bind 0.0.0.0 exposes unauthenticated local file/run APIs, so use it only on trusted networks. Upload limits are controlled by --max-upload-size, --max-total-upload-size, --max-uploads-per-run, and --allow-upload-mime; --max-concurrent-pipelines defaults to 3. --no-browser-pane disables preview/CDP support. See visual editor.

--workspace imports projects from the legacy instances.conf, pins each project to its existing run store, and hosts the registered local runtimes in one process and one browser window. Each project keeps a stable /x/<project-id>/ URL, its own environment and bot catalogue, and its own Copi conversations. Copi can request a confirmed workspace.handoff action to open a new conversation in another ready project using a short, single-use summary. That target conversation reports its verified completion or failure through a host-bound workspace.handoff.complete receipt. The workspace host persists the correlation and retries delivery to the originating Copi conversation; the notification does not switch the visible project. GET /readyz reports ready only when every pinned runtime is available.

iterion dispatch ​

bash
iterion dispatch
iterion dispatch iterion.dispatcher.yaml --port 4892
iterion dispatch iterion.dispatcher.yaml --no-server

No argument selects the native tracker, embedded bot catalogue, and HTTP :4892 defaults. See dispatcher.

iterion schedule ​

The manifest defaults to $ITERION_SCHEDULES_FILE or ~/.iterion/schedules.yaml; every subcommand accepts --manifest.

bash
iterion schedule add weekly --cron "0 2 * * 1" \
  --bot bots/sec-audit-source/main.bot --workdir "$PWD"
iterion schedule install
iterion schedule audit --name weekly --since 24h
SubcommandNotable flags
add <name>Required --cron and --bot; plus --workdir, --store-dir, --sandbox, --timeout, repeatable --var, --description, --disabled.
add <name> guards--guard, --guard-timeout, --guard-var. Exit 0 fires; stdout becomes a workflow var.
add <name> overlap--overlap skip|allow|keepalive, --max-concurrent, --stale-after.
list, removeInspect or delete manifest entries.
run <name>Execute now; --dry-run prints the resolved command.
auditFilter tick decisions with --name, --since, --surface, --tail.
install, uninstallSynchronize/remove the managed crontab block; install writes an atomic <manifest>.sh wrapper carrying the sanitized PATH and accepts --print (block preview only) and --tz. See PATH capture and limits.

See scheduling, including sub-minute keepalive behavior.

iterion issue ​

Subcommands are create, list, show, move, update, close, board, and import.

bash
iterion issue create --title "Fix auth" --label backend --priority 10
iterion issue list --state todo --unclaimed
iterion issue move ISSUE --to doing
iterion issue board show
FORGE_TOKEN=... iterion issue import --forge forgejo \
  --repo owner/name --base-url https://forge.example --token-env FORGE_TOKEN

Forge import is one-way/idempotent, skips pull requests, and reads the token only from the named environment variable. See native tracker.

iterion sandbox doctor ​

bash
iterion sandbox doctor
iterion sandbox doctor --strict workflow.bot --target local
iterion sandbox doctor --strict workflow.bot --target cloud

Strict mode resolves workflow/CLI sandbox settings and exits non-zero for driver, image, Kubernetes compatibility, host-state, or network-policy failures. It accepts the run-equivalent --sandbox, --sandbox-default-image, and --sandbox-host-state overrides. See sandbox.

iterion server and iterion runner ​

bash
iterion server --config cloud.yaml --bind 0.0.0.0 --port 4891
iterion runner --config cloud.yaml
iterion server webpush-keys        # mint a VAPID keypair for Web Push

server uses local in-process mode by default and cloud control-plane mode under ITERION_MODE=cloud. runner consumes NATS run messages and persists through MongoDB/S3. The server webpush-keys subcommand prints a fresh VAPID public/private pair for the ITERION_WEBPUSH_VAPID_{PUBLIC,PRIVATE}_KEY env vars that enable user notifications (notifications). See cloud deployment.

State, knowledge, and supervision ​

iterion secret ​

Subcommands are set, list, and rm; --project selects the per-project store. Values are never printed. set shape-checks the value at ingestion against the kind read off it (token / JSON / PEM), or the one --kind names; --kind raw stores it unchecked.

bash
iterion secret set GITHUB_TOKEN
iterion secret set DB_URL --project --hosts db.internal
iterion secret set DB_PASSPHRASE --kind raw
iterion secret list

See secrets.

iterion memory ​

Subcommands are du, export, and import. A space is selected by visibility (bot, project, cross_project, user, org, global), name, and the applicable project/user/tenant selector.

bash
iterion memory du --visibility bot --name campaign
iterion memory export --visibility project --name shared --out shared.tar.gz

See memory and knowledge.

iterion mcp ​

bash
iterion mcp                        # stdio MCP server: local_* + remote_* tools
iterion mcp --read-only            # read tools only; remote_api limited to GET
iterion mcp --only remote          # no local-store access at all
claude mcp add iterion -- iterion mcp   # register in Claude Code

Serves the operator-facing MCP server on stdio so any MCP client (Claude Code, Claude desktop, Cursor) can drive iterion: local_* tools operate this machine's store/engine (validate, launch detached runs that survive the session, follow events/logs/reports, answer questions, native board), remote_* tools drive the iterion remote instance (typed core + the remote_api escape hatch + routes/OpenAPI discovery). Flags: --store-dir, --read-only, --only local|remote. See MCP server.

iterion supervise ​

bash
iterion supervise --run-id RUN --node implement \
  --system @policies/watchdog.md --monitor event_type=tool_error,tool_name=Bash
iterion supervise install-hook
iterion supervise uninstall-hook

The watcher evaluates on turn boundaries/monitor matches and injects node-scoped steering for the next turn. Main flags are --model, --system, repeatable --node/--monitor, --cooldown, --max-evals, and --claude-session for a raw Claude Code session. A DSL supervisor declaration starts the same coordinator automatically. See supervisors.

iterion reliability ​

bash
iterion reliability report                       # resolved rollout mode + a fleet baseline over the LOCAL store
iterion reliability report --cloud --json        # the baseline a cloud promotion is decided on
iterion reliability report --cloud --tenant t1   # one tenant's cloud runs
iterion reliability report --run-id <id>         # one run's compatibility report
iterion reliability rollback                     # print the non-destructive rollback plan

Read-only operator surface for the staged workflow-reliability rollout (workflow-reliability-1006.md). report prints the mode the launch surfaces actually apply for this environment (ITERION_RELIABILITY_MODE, falling back to the older ITERION_EXECUTION_CONTEXT_POLICY), the retry-circuit settings, and either a fleet baseline over the store's runs or, with --run-id, one run's compatibility report — capture it before switching modes and again after, and compare. Run directories whose run.json cannot be loaded are listed as unreadable rather than skipped, since a baseline that under-counts silently is worse than none. Which store it reads is part of the answer, and the report's store line names it: without --cloud the baseline covers the filesystem store the working directory resolves to, which is not the population a cloud promotion is decided on; --cloud reads the Mongo run store through the same configuration the server and runner use (ITERION_MONGO_URI plus ITERION_S3_*, which the cloud store requires to open at all), and --tenant scopes it to one tenant's runs. rollback prints which variable to set and what evidence to keep; it mutates nothing, because the variables are read where the launch surfaces run (a pod spec, a service unit, a shell), which a one-shot CLI process cannot reach. Flags: --store-dir, --run-id, --cloud, --tenant, --config.

Remote, benchmarks, and utility commands ​

iterion remote exposes typed cloud domains for runs, bots, marketplace, issues/boards, dispatcher, triggers, schedules, orgs/teams/users, tokens, secrets/keys/bindings, webhooks/forge, audit/usage/limits, memory, plugins, the credential pool, SSO/admin, routes/OpenAPI, and raw API access. CI can use ITERION_REMOTE_URL, ITERION_REMOTE_TOKEN, and optional team/org selectors without a config file. The complete reference is cloud CLI.

Durable assistant missions are available under remote runs mission:

bash
iterion remote runs mission start TARGET --watch WATCH \
  --actions run.resume,run.rewind --ttl-seconds 7200 --max-actions 6
iterion remote runs mission list TARGET
iterion remote runs mission get TARGET MISSION
iterion remote runs mission stop TARGET MISSION

--invocation is the stable idempotency key; it defaults to goal:TARGET. --assistant can resolve the one exact active watch when --watch is omitted. All four commands support the remote command's normal --output json mode.

iterion bench asymptote accepts primary --runs, optional --variant-runs, a required --judge-node, judge field/threshold, loop selector, labels, title, per-run detail, and output path. See asymptote bench.

iterion map gen rewrites the three committed indexes under docs/references/ — map-packages.md (every Go package, its one-line purpose and the interfaces it exposes), map-docs.md (every page and ADR, with the ADR's status) and map-bots.md (every bundle and skill). Everything is derived deterministically: Go's own parser, markdown headings, the bundle manifest loader — no model call, no embedding. --check writes nothing and fails on drift; task map:check runs the same assertion as a Go test, so freshness rides the required test check.

iterion map build builds the graph the other map subcommands query, caching it under .iterion/map/ (gitignored — the small markdown maps are committed, a multi-megabyte graph is not). Nodes are packages, files, exported symbols, docs pages, bots, skills and .bot nodes; edges are imports, contains, declares, calls, references, links, flows and uses. Measured on this repository: 9 834 nodes and 31 366 edges in 0.7 s. The graph body is byte-identical over repeated cold builds; the artifact carries a built_at stamp, so the file as a whole is not.

Two of those relations are worth naming. references is a symbol named without being called — a parameter type, a struct field. On an interface it is the only edge there is, so a graph recording calls alone would report "nothing depends on this" about every seam in the architecture. flows is the .bot DAG, taken from the compiler rather than inferred from the text: iterion map neighbours node:<bot>/<file>#<node> and iterion diagram are two independent producers of the same fact.

The cache is keyed on a fingerprint that hashes the content of every .go, .md, .bot, go.mod, manifest.yaml and .mcp.json outside the skipped trees — content rather than (size, mtime), which is blind to a same-size edit inside one tick of the kernel's coarse clock. Any change rebuilds all of it: patching a graph in place would be faster and could describe a repository that no longer exists, which is the one failure an index must not have.

iterion bench discovery classifies each run's tool calls into orientation (read, search, list), change (write, commit) and neither, and reports the token spend of the nodes that never wrote anything plus the within-node split of the nodes that did — turn-granular, the finest the checkpoints support, never imputed. It also reconciles the two independent recordings of that spend field by field (the per-message llm_step_finished feed against the turn checkpoints), so a producing site that stopped emitting shows up as a gap on its node instead of as a coverage percentage that stays green. Takes --runs id,... or --last N, plus --output, --title and --top. Every table states its coverage, including the verbs the classifier could not name. Background and the measured baseline: context retrieval state of the art.

iterion completion <bash|zsh|fish|powershell> emits shell completion. iterion version prints build version and commit; --commit prints only the SHA, truncated to the same 12 characters the default output embeds, and exits non-zero when the build carries none (no -ldflags injection, no VCS build info, or the Dockerfile's unknown default) rather than handing a script an empty or bogus value.