meshfox — usage guidance for AI coding agents
Also available at any time via meshfox --agent-help, wherever this binary
is installed — this file ships embedded in it, so it doesn't depend on any
per-project instructions. Not part of the .canvas.md format itself; for
that, see meshfox spec.
Prefer meshfox node <verb> over hand-editing the file
A .canvas.md file is plain Markdown, so it's tempting to just open it and
edit the text directly. Don't, for structural changes — mdcanvas (the
engine behind every node subcommand) enforces invariants that are easy to
get subtly wrong by hand: single root, no duplicate ids, no dangling
meshfox:edge targets, heading-depth vs. parent= handling once a subtree
is already at H6 (Markdown's nesting ceiling), sibling order matching
on-disk position. Every node subcommand validates the whole resulting
document before writing anything back, so a bad edit fails loudly instead of
landing as a corrupt file.
Map your intent to a subcommand instead:
- Add a child node →
meshfox node add <parent-id> <title>(the root's id isrootin every canvas, unless that canvas's root declares another one —meshfox node showof any node prints its parent). Prints the new node's id (a slug oftitle) — pass--body-file <path>(or--body-file -for stdin) and/or anynode metaflag below in that same call to give it real starting content/position/style too, instead of a separatenode body/node metafollow-up that needs that id again. - Delete a node (and its subtree) →
meshfox node rm <id>; keep its children by reparenting them instead →meshfox node rm <id> --keep-children - Move a node to a new parent →
meshfox node mv <id> <new-parent-id> - Rename a node's heading text →
meshfox node rename <id> <title> - Change a node's stable id (updates every reference:
parent=,meshfox:edge from=, best-effortdeps=andmeshfox:var from=) →meshfox node set-id <id> <new-id> - Replace a node's body →
meshfox node body <id> --base-rev <rev> --file <path>(or pipe to stdin;--file -is stdin too).--base-revis required: thebody-rev:linemeshfox node show <id>prints. If the body changed since you read it (another tab, an agent, an editor), nothing is written and the error gives the current body and revision — merge, then retry with that revision. Add--mergeto have meshfox do that for you: it finds the body you read in the canvas's undo history and three-way merges your text with the current one by line. A clean merge is written and printed (read it — a line-level merge of prose can read wrong); if your edit overlaps the other one, nothing is written and the error lists the overlapping lines and the body with<<<<<<< mine/=======/>>>>>>> theirsmarkers — resolve that and write it back against the current revision. A body too old to still be in the history is the ordinary refusal. Just adding to it?node appendneeds no revision. - Append to a node's existing body, without reading it back first →
meshfox node append <id> --file <path>(or pipe to stdin;--file -is stdin too) - Set position/size/style/tags (
x/y/w/h/color/type/display/lang/interpreter/tags) →meshfox node meta <id> [flags...]— same flagsnode addabove also accepts.coloris either a literal hex string or one of six numbered presets — the same ones the web UI's swatch picker uses — so--color 4is shorthand for green:1red,2orange,3yellow,4green,5blue,6purple- e.g.
meshfox node meta <id> --color 4for green, or--color ""to clear it back to no color - tags are only ever added or removed, never replaced as a whole list:
--add-tag "bag,fixed"/--remove-tag "old"(comma-separated, same spelling as the file's owntags=). A tag already there keeps its place. (node addtakes--tags "bag,fixed"for a new node's initial tags;node meta --tagsis gone.)
- Add or remove a node's extra incoming edges →
meshfox node edges <id> --add <id>... --remove <id>...(never replaces the whole set; adding an edge that's already there leaves its label and route alone) - Resync on-disk heading order to match on-canvas layout after moving things
by position →
meshfox node reorder - Inspect a node before changing it (parent, children, extra parents, type,
position/style, tags/color when set) →
meshfox node show <id>, instead of reading the raw file and guessing
Hand-editing is fine for prose inside an existing node's body that doesn't touch structure or bookkeeping comments. Either way, validate afterward:
meshfox validate <file>
It runs the same parse checks every other command does, without executing or
writing anything, and exits non-zero on failure — safe to chain in a script
or run right after any edit, by hand or via node.
If the document has any embedded ```starlark constraint fences (a
Starlark contract over the tree, living in any node's own body, e.g.
"every node tagged table has exactly one file child" — see meshfox spec's "Constraint fences"), also run:
meshfox check <file>
This is a different check than validate: validate only asks whether
the file parses; check asks whether the document actually satisfies its
own declared rules. A structural edit that leaves the file well-formed can
still break a constraint (e.g. deleting a table's only file child) —
check is what catches that, validate won't.
check also evaluates every ```starlark render fence and fails on one that
raises an error; meshfox render <file> [--node ID] [--name NAME] prints what
those blocks compute, to read or verify a computed view without a UI.
Running non-interactively
meshfox configure and any unresolved meshfox:var prompt during run
require an interactive terminal — don't use them. Supply variable values
directly instead:
meshfox run <path...> <block> --set NAME=VALUE --set OTHER=VALUE
--no-deps skips a block's deps= chain and runs only what's named on the
command line, if that's what's intended instead of the full chain.
Other discovery commands
meshfox list— every runnable code block as a ready-to-pastemeshfox runinvocation, instead of reading the file to find block names.meshfox spec— the full.canvas.mdformat reference, if a hand-edit is actually warranted.meshfox node find— the general lookup tool: a CSS selector (.tag,[type="file"],#parent > .child) for structure,--text <substring>for a case-insensitive full-text search over title/body (each match comes back with a short excerpt, so a separatenode showusually isn't needed just to see why it matched), and--created-after/--created-before/--updated-after/--updated-before/--since(RFC3339 or a relative duration like7d) for filtering by when a node was made or last changed. All of these AND together; any can be omitted, and the selector itself defaults to*(every node) when dropped — e.g.meshfox node find --since 7dfor "what changed recently," instead of rereading the whole tree every session.
MCP, if available
meshfox mcp is a stdio MCP server — experimental, and not something this
binary itself starts (a host launches it: {"command": "meshfox", "args": ["mcp"]}). It takes no arguments; whichever directory it's started in
becomes its root. It's multi-canvas: canvas_open/canvas_close/
canvas_list manage a registry of open canvases (each its own isolated
spawned process — a crash or hung debug session on one canvas can't touch
another), and every other tool requires a canvas_id as its first
argument — there's no implicit "current" canvas. A canvas_id is the
file's path relative to the root, so a call with the id of a canvas that
isn't open (never opened, closed after sitting idle, or whose process died)
just reopens it; canvas_open is only needed to create a file or to get the
id. A canvas whose process is hung (alive, not answering) is stopped after
three unanswered pings or a call with no answer in 90 s and reopened by the
next call; canvas_list reports each canvas's busy and health and never
waits on one. canvas_open only resolves files under that root directory;
nothing above it is reachable. If this session already has that configured, its tools are
usually a better fit than shelling out through this same CLI for two
specific cases:
- Running a block (
run,session_reset):runexecutes a block with itsdeps=chain and returns every step's exit code, duration and output tail as JSON. A dependency that already ran this session and looks unchanged is skipped — for a build/test step that depends on files rather than on the block's own text, passfresh: true(the whole chain runs for real this once; nothing is forgotten), or callsession_resetto forget everything. The CLI equivalents aremeshfox run --freshandmeshfox session reset. Blocks markedconfirmrequire explicit user approval for each invocation, including when reached as dependencies. Ifrunreports confirmation required, ask the user before retrying withconfirm: true; never infer approval from a prior run. CLI uses--confirmfor automation. Aformfence is sent, not run:form_submit(CLI:meshfox form submit NODE FORM --set VAR=VAL) commits the values into the session and starts theautorunblocks they reach and waits for them (exit codes and output come back;wait: false/--no-waitto skip that), whichrunwithvarsdoes not;form_reset(meshfox form reset NODE FORM) puts the fields back to their defaults and starts nothing. A long chain can outlive a tool call (and its result can be too big to read): passdetach: trueto get thechain_idback at once, then read the run withlogs(id= that chain_id; pass the previousnext_seqassince_seqto follow it) instead of waiting.logswithlist: truelists past invocations andstatssummarises them;skip_fresh: trueskips a still-fresh block (CLI:--skip-fresh,-d/--detach).listshows the runnable blocks andrenderthe Markdown ofstarlark renderfences. - A multi-step debug session (
debug_start/debug_send/debug_stop): a persistent shell in a node/block's own resolved cwd/env, so state (exported vars, files a snippet wrote) survives between calls — a one-shotmeshfox runre-resolves everything from scratch every time. - Structured reads/single edits — the whole
node <op>surface, not just a subset:node_show/node_find/node_add/node_body/node_append/node_meta/node_block/node_rm/node_mv/node_rename/node_set_id/node_edges/node_move/node_reorder—node_showreturns JSON, not text to re-parse, including each node'sbody_rev:node_bodyrequires it back asbase_revand refuses a body that changed since (the error carries the current body and revision to retry with);merge: truemerges instead — seemeshfox node body --mergeabove — and returnsmerged: truewith the storedbodyand itsbody_rev. validate/check— same checksmeshfox validate/meshfox checkrun, as structured JSON instead of text to re-parse:validatereturns a tool error naming the specific problem on failure (same conventionnode_addetc. use for a bad write), not just a non-zero exit code;checkreturns{ok, results}with one entry per constraint fence (ok/messages) — a single constraint failing isn't itself a tool error, so a caller can inspect what's wrong without try/catch.
Everything else (list/static/pdf, batch edits, anything --no-deps-shaped)
still only exists via the CLI below — the MCP tools are a narrower,
additive surface, not a replacement for it.
Canvas path
Most commands take the canvas path as an optional trailing argument or
--canvas flag (run and node <op> only take --canvas, not a bare
trailing path — their own positionals are node-id/block-name args instead).
The path can also be given once, before the subcommand instead of after it
— meshfox path/to.canvas.md run tests smoke — which a subcommand's own
path/--canvas overrides if both are given. Omitting it entirely
auto-discovers the single .canvas.md candidate in the current directory — fine for a one-off in a
directory known to have exactly one, but pass it explicitly whenever that's
not certain, so behavior doesn't depend on what else happens to be in the
directory. run foo.md tests smoke (path after the subcommand instead of
before) is a common slip — foo.md there just gets swallowed as if it were
a node-id path segment instead, since run's own positionals have no such
convention of their own; both this and a "did you mean" typo/missing-
ancestor suggestion are surfaced back in the error when it happens, so it's
recoverable, but getting the ordering right the first time avoids the
detour.
Terminal status for CLI runs
meshfox run --terminal-status auto|off|iterm2|progress|title controls the
terminal's run indicator. MESHFOX_TERMINAL_STATUS supplies the default;
the flag takes precedence. This is a CLI presentation option, not a canvas
or worker setting.
Auto selects Session Status (OSC 21337) for iTerm2 and OSC 9;4 progress for
Ghostty, Contour and Windows Terminal, using TERM_PROGRAM and WT_SESSION.
Unknown terminals and tmux/screen are disabled in auto mode. Explicit modes
can override detection, but stderr must still be a terminal. Older terminal
versions may ignore the sequences. No multiplexer passthrough is configured.
Runs show the current step; meshfox's own input prompts show waiting. Interactive child programs remain running: their input waits are not inferred. Success/failure remain visible in iTerm2; progress is withdrawn on success and marked as failed on error. Handled Ctrl-C clears the indicator. Progress is refreshed once per second for terminals that expire stale state. The optional title mode changes the terminal title; the shell may replace it at its next prompt. Status sequences are written to stderr, never stdout.
meshfox tui --terminal-status auto|off|iterm2|progress|title uses the same
option and environment default. It aggregates the current canvas's local
and external runs, including TTY sessions. Input forms and run confirmations
show waiting; normal navigation does not. Parallel runs retain a failure
until a new batch starts. Finished runs already present when opening a canvas
do not count as a new result. Nested canvases retain independent results.
Healthy background services show ready: N services in iTerm2/title mode;
starting/stopping/restarting services count as activity, and a crashed service
or failed stop shows failure. Progress-only terminals withdraw the indicator
when services are ready. TUI exit and handled shutdown signals clear status.
Observe an existing core
meshfox console [CANVAS] (or meshfox CANVAS console, or --canvas CANVAS)
attaches to an already running core. It never starts a core or executes blocks.
Without a path it discovers a single .canvas.md file by name, without reading
canvas contents. For a marked plain .md canvas, pass its path. A missing core is an
error; a configured but unreachable coordinator is not bypassed.
The console replays buffered output of running plain blocks, follows new runs,
and reports service state and bounded service-log snapshots. Historical finished
runs at initial attachment are omitted. TTY sessions are outside its scope.
Output carries node::block prefixes; stderr output goes to stderr. Service
snapshots are reconciled by their overlapping lines (identical repeated lines
in a completely rolled-over buffer cannot be distinguished).
--terminal-status auto|off|iterm2|progress|title and
MESHFOX_TERMINAL_STATUS work as for run/TUI. The indicator aggregates active
blocks, failures and ready services, shows waiting while disconnected, and
clears on exit. Ctrl-C detaches without cancelling runs or stopping services.
The console retries discovery after connection loss, including port changes.
Rust passive run subscriptions resume with sequence cursors and a persisted
run identity, so reconnecting cannot splice another invocation's output into
the original stream. Workers without persisted run identities cannot resume
that stream safely.
Saved block and chain logs
meshfox logs # latest chain in the discovered .canvas.md
meshfox project.canvas.md logs # explicit canvas (also --canvas PATH)
meshfox logs build # latest participation of block build
meshfox logs tests smoke --chain # latest chain requested for tests::smoke
meshfox logs --list # list chain runs, newest first
meshfox logs build --list # invocations involving this block
meshfox logs build --chain --list # invocations requested for this target
meshfox logs --id 42 # an immutable chain transcript
meshfox logs --id 42 --block tests smoke # only this block inside chain #42
meshfox logs --id 42 --effective # expand outputs used by skipped steps
meshfox logs build --chain --follow # replay and follow this invocation only
meshfox logs --id 42 --json # NDJSON: run header, events, final status
With a block address, logs selects its latest participation in an admitted
invocation, whether it ran directly, as a dependency, or was skipped. --chain
selects by the requested target and shows the entire invocation; without an
address the latest chain is selected. A CLI run a,b submits one chain per
target and therefore records separate invocation IDs. --id always names a
chain, including when --block narrows its transcript. Historical block
selection remains available after the block is edited or removed.
Chain status for monitoring
meshfox status # per-step table of the latest chain
meshfox status --id 42 # a particular chain
meshfox status build --chain # latest chain requested for build
meshfox status --json # one JSON snapshot, no ANSI
meshfox status --watch --json # NDJSON events until the chain ends
status folds the saved chain history that logs reads into one row per step
(pending, running, ready for a service that is up, succeeded,
failed, skipped, killed, not-started) with durations, exit codes and test totals recognised from
cargo test, Playwright and node --test summary lines. A failed or killed
step also lists its failing tests and the last lines of its output. Exit code:
0 for a running or succeeded chain, 1 for a failed, killed or interrupted one,
2 when no chain can be read. With --watch it keeps polling and exits with the
chain's code once it ends.
--json prints no terminal formatting. Without --watch it is a single
{"type":"snapshot","run":{...},"steps":[...],"current":"node/block","failures":[...]}
object. With --watch it is NDJSON: a first snapshot, then a step line
whenever a step changes state, a failure line when a step first fails (with
tests and tail), and a final done line with the run and exitCode. Pipe
it into a monitor instead of grepping logs.
The default view records what happened in that invocation: output, stderr
labels, step results, freshness skips, service startup/reuse, errors, and steps
not reached. It does not substitute old output for a skipped step.
--effective expands an immutable snapshot of the exact successful block run
used by each freshness skip, labelled with its original block-run ID and time.
A later rerun cannot change it. If that source's output was already unavailable
when the skip was recorded, the transcript says so; it never substitutes a
newer run. Chain transcripts are independent of per-block history rotation.
--follow first replays the selected transcript, then follows that same ID
until completion, cancellation or interruption. A finished run returns at once.
It never follows a subsequent invocation; use console for that. Ctrl-C only
detaches. Sequence cursors prevent duplicate output across reconnects, including
worker port changes. The command accesses history through the worker API and
can start a worker to read it after the previous one exited; it executes no
blocks. Reading a failed run is a successful logs command (exit 0); missing
history and read errors are nonzero.
The worker records admitted chains for CLI, TUI, web, autorun and native canvas
calls in its existing session database. Existing block-history rows are not
retroactively reconstructed as chains; invocation transcripts begin with this
version. TTY terminal bytes and a service's ongoing log after readiness are
not part of a chain transcript (see console for service logs). Read-only
canvases retain transcripts only for their worker's lifetime.
[session] max_chain_runs defaults to 100 completed invocation transcripts,
independent of max_runs_per_block. [session] max_chain_output_bytes defaults
to 1048576 bytes of output per transcript, including effective output. It keeps
the recorded prefix and always retains step/completion metadata; truncation is
explicit. max_chain_output_bytes = 0 disables storing transcript output;
max_output_bytes = 0 disables it too. No request variable values are saved as
invocation metadata. A worker restart marks unfinished transcripts interrupted
and preserves their recorded prefix. Output is committed in batches of up to 64
events or 50 ms; control events flush the preceding output immediately. A storage
failure marks the transcript incomplete without stopping execution. Normal session reset invalidates freshness
but does not erase these historical transcripts.
Statistics from saved journals
meshfox stats # overview of this canvas
meshfox project.canvas.md stats # explicit canvas (also --canvas PATH)
meshfox stats build --chain # chains requested for build
meshfox stats tests smoke # only the tests::smoke block
meshfox stats --since 7d # invocation start time, relative or RFC3339
meshfox stats build --chain --json # complete machine-readable report
Statistics use only retained invocation journals. The report always shows the
number and oldest/newest start times of the retained chains, plus the number
and time span matching the selection. --since does not recover rotated-out
history: a requested month may contain only the most recently retained runs.
Empty selections succeed with zero counts and null duration/rate fields in
JSON (n/a in text). This is not an all-time counter.
A block address selects that block's participation across retained chains,
including dependencies and freshness skips. --chain selects chains whose
requested target matches the address and includes their dependency steps.
These address rules match logs; removed block definitions remain selectable
by their stored address. The owning worker computes one metadata snapshot via
GET /api/stats; the CLI never opens the canvas or session database. Starting
a worker to read statistics does not execute blocks. There are no new database
tables or stored aggregates, and the logs list's 1000-row limit does not cap
statistics.
Success percentage is succeeded / (succeeded + failed). Killed, interrupted,
running and unknown outcomes are shown separately. Freshness skips, unreached
steps and service startup/reuse do not count as successful block executions.
Block duration samples use successful/failed step-end events with a recorded
duration; chain duration samples use the admission-to-terminal timestamp span
of succeeded/failed chains. Running, killed and interrupted invocations have no
chain duration sample. A completed step in a running chain can still contribute
its completed block duration. Chain and step times are never added together.
Median averages the middle pair for even samples; p95 uses nearest rank
(ceil(0.95 * N)). Zero samples are n/a, not zero milliseconds.
The overview ranks the five blocks with the largest total completed step time and the five with the most known failed attempts; JSON includes every block. The block view also shows its last twelve observed outcomes with chain IDs and times in JSON. Skip percentage is skipped / (attempted + skipped), excluding unreached/pending steps and successful service-start/reuse markers. Estimated work avoided sums the known durations carried by skips and is labelled as an estimate; it is not a claim about saved wall-clock time. Incomplete transcripts are flagged, with unknown outcomes/unobserved planned steps kept distinct from successes. Output truncation or disabled output storage does not discard the retained step metadata used by statistics. Text output uses the same terminal colour rules as logs; JSON and redirected output have no added ANSI styling.
Native calls between canvases
A meshfox fence is YAML with required canvas and run, optional set
(string values), fresh, no-deps and skip-fresh. It uses a native server
executor, not a shell wrapper or a nested CLI. Child document variables and
confirmation gates are prepared before parent side effects. Child events carry
optional child: {canvas, port, event} metadata; CLI/TUI terminal status reads
this metadata to show the child step. Cancellation names the exact child run.
See SPEC.md's "Calling another canvas" and examples/canvas-call.canvas.md
for the runnable parent/child example with an embedded SVG architecture diagram.
Required child application arguments must be bound in run. TTY relay is not
supported; service-starting calls need an independently running child worker.
The parent's YAML cannot auto-approve child confirmation gates.