CLI reference

The commands and flags a user reaches for, with a pointer to per-command help.

The commands and flags a user reaches for. agni <command> --help always prints the authoritative, up-to-date detail. This page is the map. The reader for a file is chosen by its extension, so you pass the design file directly and never name a format.

Where a <file> is expected you may also name a design folder, one holding a design.yaml that declares which file is the design’s entry. Agni then reads that entry and picks up the companion views the descriptor lists, so a netlist’s connectivity rules and a board’s copper rules run from one argument. See Projects and designs.

The commands fall into four groups by what each one takes as its argument.

The agni commands grouped by what they take. Seven commands read one design and report on it. One compares two revisions. Two host the browser viewer. Three work on a results document written by an earlier run, which stays readable after the design is gone. READ ONE DESIGN COMPARE TWO REVISIONS HOST A VIEWER KEEP A RUN a design file or a design folder an older revision and a newer one a design and the project it is in a results document from an earlier run agni start scaffolds the folder form stats check review query render intake emit diff open serve results results --compare import-results component, section, and net counts findings, gated by --fail-on one outcome per checklist item rows of facts, each with its provenance the schematic or the board, svg or pack a sanitized summary, no net names in it the same design as IPC-2581 what changed, as text, json or csv one design, prints its URL named mounts, the whole viewer renders it with the design deleted the three-way entity split writes one from another tool's report Not shown: the global flags, and the developer commands native, validate, census and derive.

Commands

stats <file>

Print component, section, and net counts for one design. Run it first to confirm the tool read your design the way you expect before trusting anything downstream.

check <file>

Run the rule catalog and report findings. The workhorse. See Checks and reports.

flag what it does
--rule <name> run only this rule (repeatable)
--tag <key>=<value> run only rules with this tag, e.g. --tag category=power (repeatable)
--format <fmt> text (default), markdown, json, csv, report, or html. csv emits one row per finding with a stable header, for a spreadsheet-driven review. html is the verdict report as a self-contained page and turns --verdicts on by itself
--verdicts report the CONSIDERED SET instead of the violations: what each rule concluded about every subject it looked at, with the evidence for a pass. A separate table, so plain --format csv is unchanged. Honours --format text/csv/json/html. Only rules that state one contribute, and a rule absent from the output is declining to say rather than reporting that it considered nothing
--server <where> where the links this run mints should point. Empty (the default) mints none, which is what a pipeline wants. self starts a viewer on a free port, serves THIS run’s mount table, and blocks until Ctrl-C, so a link cannot disagree with what was read and a mount minted for the run is linkable like any other. self:PORT does the same on that port and fails if it is taken. A URL names a server someone else is running, which is asked whether it serves the same mounts from the same roots
-o, --out <file> write the --format output to this file, - for stdout (the default), so a report needs no shell redirect and composes with a pipe. Distinct from --results-out: this writes what you would have redirected, that writes the check-result DOCUMENT agni results re-renders. The written-file note goes to stderr
--fail-on <sev> exit non-zero when a finding sits at or above error / warning / info. This is the severity axis. For the coverage axis, see review --fail-on-outcome and --min-answered below
--conventions <file> compose a naming-convention config into the run (see Naming conventions)
--profile-path <dir> compose a directory of YAML interface-profile declarations into the catalog, namespaced profile-overlay/ (see Interface profiles)
--params <dir> load a datasheet parameter set, enabling datasheet-backed rules (see Datasheets)
--board-path <file> attach a board that is NOT a declared companion of the design, so board-tier rules resolve instead of finding no copper

review <file>...

Run a review checklist over one or more designs and report one outcome per item. Where check asks “what is wrong with this board”, review answers “which of our questions did we actually answer”. Its outcome vocabulary distinguishes a check that passed from one that never ran.

flag what it does
--checklist <file> the review manifest (YAML) declaring areas and their items. Optional when the design belongs to a project that declares one; passing it overrides the project’s
--conventions <file> a naming-convention config, whose rules join the catalog and whose lexicon reaches the design read
--profile-path <dir> interface-profile declarations added to the catalog
--params <dir> a datasheet parameter set, enabling datasheet-backed items
--intent-path <file> a design-intent declaration, so intent-bound items resolve instead of reading needs-design-intent
--board-path <file> a board-geometry file attached to a netlist design, so board-tier items resolve instead of n/a
--coverage a per-area rollup of how many items each area decided, instead of the per-item report. Renders as markdown only, so it refuses an explicit --format; the --format html page carries the same rollup in its header
--ratified-floor <n> datasheet-confidence floor below which a fail reports as provisional (default 0.9)
--fail-on-outcome <list> exit non-zero when any item sits at one of these outcomes, e.g. fail or fail,provisional. Off by default
--min-answered <n> exit non-zero when fewer than n items produced an answer. Off by default
--format <fmt> markdown (default), json, or html. html is the checklist as a self-contained page: areas and items in the manifest’s order, every finding per item rather than the markdown cell’s first three, and one link per finding when --server is given. One design at a time, since a page’s title, hash and links all name one design
--server <where> where the links this run mints should point. Empty (the default) mints none, which is what a pipeline wants. self starts a viewer on a free port, serves THIS run’s mount table, and blocks until Ctrl-C, so a link cannot disagree with what was read and a mount minted for the run is linkable like any other. self:PORT does the same on that port and fails if it is taken. A URL names a server someone else is running, which is asked whether it serves the same mounts from the same roots
--results-out <file> also write the run as a self-contained check-result document
-o, --out <file> write the --format output to this file, - for stdout (the default), so a report needs no shell redirect and composes with a pipe. Distinct from --results-out: this writes what you would have redirected, that writes the check-result DOCUMENT agni results re-renders. The written-file note goes to stderr
--render <dir> also write an annotated schematic SVG per design, each finding highlighted in place
--companion <file> a geometry file to draw --render images on, joined to netlist findings by net name

Gating a pipeline on a review

check --fail-on and the two flags above gate on different axes, and the difference is not a matter of taste. --fail-on pivots on finding severity, which states how bad an answer was. --fail-on-outcome and --min-answered pivot on item outcome, which states whether the question was answered at all. A checklist can stop answering four of its items with its failure count unchanged at zero, and no severity predicate can see that.

--min-answered counts the items that produced an answer: pass, fail, provisional, and computed-n/a. It is deliberately stricter than the covered count the report also shows.

Why answered is stricter than covered

Covered subtracts only not-automated, which moves when a rule leaves the catalog. It does not move when a rule is present and its inputs are gone. A datasheet-backed item whose corpus moved reads not-applicable, which still counts as covered and does not count as answered. That gap is the regression worth gating on.

A provisional does not trip --fail-on-outcome fail. It is a failure resting on mock or below-floor datasheet data, so gating on it by default fails a pipeline on data quality rather than on design quality. Name it explicitly when you want it: --fail-on-outcome fail,provisional.

With several designs the gate reads every one of them, and the first design to violate it stops the run. Exit codes:

code meaning
0 the run completed and no gate tripped
2 a gate tripped
1 the run itself failed: an unreadable design, an invalid manifest, an unknown outcome name

check --fail-on uses the same codes, so one CI script can treat every gate in this CLI alike: 2 means the design has problems, 1 means the tool could not do its job.

Machine configuration: agni.yaml

The flags that say where bytes are can live in a file instead of on every command. agni.yaml is searched for beside the working directory, upward a few levels, then in $XDG_CONFIG_HOME/agni/ (or ~/.config/agni/). The first one found wins outright.

# agni.yaml
mounts:
  boards: /srv/boards
  shared: /srv/shared
symbol_paths:
  - /usr/share/kicad/symbols
web_dir: /usr/share/agni/web
native_tools:
  - kicad-cli

Paths are resolved against the directory you ran from, and a mount naming a directory that is not there is an error, so a file meant to serve every working directory wants absolute paths.

A run says which file it took config from, on stderr. An explicit --mount, --symbol-path, --web-dir or --enable-native wins outright rather than merging: naming a mount table is answering for the whole table.

web_dir is where the viewer’s own assets are. It belongs to this tier because it locates bytes and cannot change what a run concludes, and a wrong value fails at startup rather than quietly. A repo checkout needs nothing here, since the default web already resolves per-directory; an installed binary run from a design folder has no relative answer, and that is what this and AGNI_WEB_DIR are for.

native_tools is the file form of serve --enable-native, and belongs here on the same test: it says which golden renderers EXIST, and naming one that is not installed fails at the point of use with the tool’s own name in the error. Only serve consumes it, though every command reports having read it, because the note says what the file supplied rather than what the command went on to use.

It carries only tier-1 config, and that is a boundary rather than a to-do. Naming conventions, interface profiles, seeded parameters, design intent and a review checklist belong to a project instead. See Projects and designs.

Why analysis config cannot live here

Those five decide what a design is checked against, so they belong where they are scoped to the designs that declared them. A machine-wide conventions file applying to every design a CLI opened is the bug per-design config fixed. Unknown keys are rejected, so reaching for conventions: here is told no rather than quietly becoming a global analysis tier.

start <design-file> [dir]

Scaffold a review project around an existing design file, so the commands above can stop taking flags. dir defaults to the current directory.

agni start boards/gateway.edn ./gateway-review
gateway-review/
├── project.yaml            declares the project's id
├── conventions.yaml        stub — your team's naming vocabulary
├── review.yaml             seeded from the shipped catalog
└── designs/gateway/
    ├── design.yaml         names the entry and its companion views
    ├── gateway.edn         copied
    └── gateway.kicad_pcb   copied, declared as a companion

After it, agni check gateway-review/designs/gateway and agni review … resolve the whole configuration from the descriptors.

flag what it does
--name <id> the project’s declared id; defaults to the target directory’s name, lowercased with anything outside [a-z0-9._-] replaced by -
--title <text> the human-readable label; defaults to the id

The design is copied, and the project owns its copy. Editing the original afterwards does not reach the project. The command prints what it copied and from where, and the generated design.yaml records the origin in a comment.

Companions are detected, not guessed at. A sibling is declared a companion only when it shares the design’s stem and carries schematic geometry or a board. That excludes a later revision (gateway-rev-b.edn, a different stem and a legitimate analysis source of its own) and a second netlist encoding (gateway.edf, same stem but no view to contribute). Check the generated design.yaml and edit it: membership is declared per file precisely because it cannot be inferred reliably.

Nothing is overwritten. An existing file stops the command and is named, and every planned write is checked before any write happens, so a refusal leaves nothing half-created. Pointing at a folder that already holds a project.yaml adds the design to that project rather than nesting a second one.

intake <file>

Extract a sanitized summary of a design: counts, class census, rail voltages, anomalies, and the parts list. It carries the shape of the design and structurally cannot carry a net name or a connection, so it is safe to hand to someone who should not see the design itself.

flag what it does
--params <dir> a parameter set, which populates the MPN and datasheet-gap columns. A design inside a project reads the params/ that project declares and needs no flag; this names a corpus for a design that belongs to none
--parts <view> types (BOM by distinct part type, default) or full (per-component AVL)
--format <fmt> md (default) or json

params <mpn>

Print one part’s whole datasheet record: its source documents, every parameter with its limit kind, bounds, test conditions and citation, the pins the datasheet declares, and any constraints between them.

This is the record behind a query answer. The datalog relations carry what a query can bind (param, param.range, param.typ, param.pin); the conditions a value is valid under, the pin bindings, the full provenance and the verification state live here.

It needs no design, because a spec library is not a design. Name a corpus with --params, or name a design with --design so the project that design belongs to supplies its own params/.

A parameter someone has verified reports whether that still means anything. When the corpus has moved to a later revision of the document, the row reads stale and names BOTH revisions: the one that was checked, and the one held now. That pair is the re-confirm task; a pair of content hashes is not.

flag what it does
--params <dir> the datasheet corpus to read. A project’s own params/ wins over this when --design names a design in that project
--design <path> a design whose project supplies the corpus, for a part seeded in a project rather than a loose directory
--format <fmt> text (default) or json. json emits the PartSpec itself, so a script binds to the same message the viewer carries
agni params LM1117 --params seed/
agni params LM1117 --design designs/gateway/gateway.kicad_sch
agni params LM1117 --params seed/ --format json

results <file>

Render a check-result document written earlier by check --results-out or review --results-out. The document is self-contained, so this works with the design deleted.

flag what it does
--format <fmt> the same output formats the live run offers
--compare <file> compare against another results document and print the three-way entity split instead of a report

import-results <report.json>

Read another tool’s check report (a kicad-cli DRC or ERC JSON report) as a check-result document, so it can be rendered and compared against a run of this engine with results --compare.

query <file> <query>

Search the design as data with an ad-hoc datalog query. Each answer prints with its provenance. See Querying your design.

flag what it does
--params <dir> a parameter set, to query datasheet facts (param(...))
--conventions <file> apply a naming convention’s LEXICON to the read, so rail/feedback/pin.type answer under your project’s vocabulary (see Naming conventions). The rules half is unused here: a query runs no rules
--board-path <file> attach a separate board export so the board.* relations have facts; without it they are empty
--format <fmt> text (default aligned table), csv (spreadsheet-safe, header row, table only), json (protojson of the RunQueryResponse the RunQuery rpc returns, so a script and a client parse one shape; it carries the column kinds, and echoes the query and the design so an answer names its own question), markdown or html (a view: the question above its answer). See Taking a view out of the tool
--title <name> name the view, used as the heading in --format markdown and html
-o, --out <file> write the --format output to this file, - for stdout (the default). A view is a thing to hand to someone, so it wants a filename rather than a redirect. The written-file note goes to stderr

trace <file>

Follow a signal from one pin to another and print what it goes through. See Following a signal.

The walk crosses series pass elements (resistors, inductors, ferrites, fuses), because those split a net without breaking the path. A capacitor is a DC block and is never crossed. A rail or a plane may be where a route ENDS and is never passed through, so a trace onto a supply reports the supply and stops there.

Both endpoints are pins, named <ref-des>.<pin>. An endpoint that names nothing the design has is an error and exits non-zero, because a pin spelled wrong in a declaration is not the same answer as two pins that are not connected.

flag what it does
--from <ref.pin> the pin to start at, e.g. U7.3
--to <ref.pin> the pin to end at
--hops <n> how many series crossings to search through (default 6). Unlike the protection radii this is a search budget rather than an electrical claim, and every answer states the value it rests on, so a no-route can be re-asked wider
--format <fmt> text (default), or json, which emits the same Trace message the TraceDesign rpc returns, in protojson, so a script reading the CLI and a client reading the API parse one shape
--server <where> where the links this run mints should point. Empty (the default) mints none, which is what a pipeline wants. self starts a viewer on a free port, serves THIS run’s mount table, and blocks until Ctrl-C, so a link cannot disagree with what was read and a mount minted for the run is linkable like any other. self:PORT does the same on that port and fails if it is taken. A URL names a server someone else is running, which is asked whether it serves the same mounts from the same roots
-o, --out <file> write the --format output to this file, - for stdout (the default), so a report needs no shell redirect and composes with a pipe. Distinct from --results-out: this writes what you would have redirected, that writes the check-result DOCUMENT agni results re-renders. The written-file note goes to stderr
--render <file.svg> also draw the answer: the route’s nets and the parts crossed, on the design’s own schematic where it has one and on an auto-layout of its netlist where it does not, which it says so you never take the second for the first. A no-route draws too, marking the two nets that fail to join

diff <old> <new>

Structural diff between two revisions, over the IR. See Comparing revisions.

flag what it does
--format <fmt> text (default human summary), json, or csv (one row per change, with a change_class column naming which kind it is)

render <file>

Draw a design’s schematic or board view.

flag what it does
--layout <name> faithful (default, the design’s own geometry) or an auto-layout (force, grid, layered, orthogonal, stress) computed from the netlist
--format <fmt> svg (default) or pack (for the WebGL viewer)
-o <file> output path

open <design>

Serve one design and print the URL that shows it, so a board can be looked at from the directory it lives in. Binds loopback on a free port and serves only that design (and its project, where it has one). It also prints a ready-made agni check … --server … line, which carries --mount because a mount is minted per process and a second agni would not know this one.

agni check <design> --server self is the same idea from the other end and needs no second command: one process reads the design and serves it, so the links resolve without anything being declared. open remains the way to look at a board without running anything over it.

flag what it does
--addr <addr> listen address; empty picks a free port on loopback
--web-dir <dir> the viewer’s own assets, as for serve

serve

Host the browser viewer and the web API on one port. Build the web bundle first.

flag what it does
--addr <addr> listen address (default :8080)
--web-dir <dir> the viewer’s OWN assets, not designs (default web; then web_dir in the nearest agni.yaml, then AGNI_WEB_DIR)
--mount <name>=<path> expose a design folder in the file browser (repeatable)
--theme <name> render palette: default or dark
--profile-path <dir> compose interface profiles into the catalog every rule-running surface uses, the check panel included (see Interface profiles)
--review-store <dir> a writable directory that keeps review runs, created if absent; without it the review endpoints report that this server stores none (see Running the server)

healthcheck

GET /healthz on a running server and exit 0 only on a 200. It is what the container image’s HEALTHCHECK runs, so the image needs no curl or wget: the runtime is debian-slim, which ships neither, and pulling one in to make a single HTTP request adds surface area to every deployment for the sake of a probe the binary can make itself. Running the server covers where it fits in a container.

flag what it does
--addr <host:port> the server to probe (default localhost:8080). serve’s own default is :8080, which binds every interface and is not a dialable host, so the probe names loopback explicitly
--timeout <duration> how long to wait for a response (default 3s)

It asks one question and interprets nothing: whether the server answers 200 on /healthz, which is the whole question a restart policy acts on. It reads no other route and does not look at the body.

emit <in> [out]

Convert any design the tool reads into IPC-2581 or an EDIF netlist (stdout if out is omitted). A conversion is A -> IR -> B with the IR as the pivot, so the input format does not have to match the output one.

Flag Meaning
--format <fmt> ipc2581 or edif. Omitted, the OUT file’s extension decides: .edn, .edf and .edif write EDIF, everything else writes IPC-2581. Writing to stdout has no extension to read, so it writes IPC-2581
agni emit board.kicad_sch board.edn

Both writers are lossy-bounded and state their limits, so a conversion carries what the IR holds rather than what the source file held. The EDIF writer emits the netlist tier only. A hierarchical design writes out FLAT, because the reader scopes a read to the design’s top cell and drops every sub-cell’s contents before the writer sees it, and array bus declarations are not written because the diagnostic they reach the IR as does not record which port declared them.

An EDIF instance needs a name, because a net anchors each of its pins with an instanceRef naming one. The writer prefers the id the source used, falls back to the ref-des (with the section index appended for a multi-gate part), and breaks a collision by suffixing, so the names are unique across the file and stable between two exports of the same design. Neither seed is unique by itself: two of the readers record no instance id at all, and a KiCad symbol placed on two sheets of one hierarchy carries the same id under two ref-des.

One connection shape has no instance to name. A KiCad power symbol or PWR_FLAG reaches the IR as a connection on #PWR01 while the component list stays physical, so there is nothing in the contents to anchor it to and minting one would invent a component the source never had. Those are written as bare portRefs and read back as connections with no component, which is what an EDIF no-ref connection has always been. Every other connection is anchored.

The output is written to be read by tools other than this one, which is a stricter target than round-tripping through agni’s own reader and was not always met. Three things follow from it. The libraries come first and the design node last, so a reader that resolves a reference the moment it meets one is never asked to follow a forward reference. A portRef names the cell’s PORT rather than the pin’s physical designator, with the mapping between them carried per instance in a portInstance table, which is the construct EDIF has for it. And an identifier holds no character a reader rejects, with anything richer moved into the quoted display half of a (rename ...).

Anything the file references, it also declares. Where a part type declares fewer pins than the netlist connects, the cell’s interface is completed from the connections; where no part type exists at all, the cell is declared too, and so is the top cell the contents live in. A board file is the case that needs all three, because copper carries footprints, pads and nets and never says what the part is. This states what the cellRefs and portRefs in the same file already name rather than inventing a part, and it is bounded the way a netlist is: a pin on no net is invisible, so a cell completed this way carries the pins the design uses and not the pins the part has.

A part named gateway:CONN4 inside a library called gateway is written as cell CONN4, since EDIF’s own library scoping already carries the qualification and a reader that treats the qualified form as one name rejects the colon in it. The prefix stays in the IR, where it selects the symbol file an external reference resolves from, so this is an encoding choice in the writer and not a rename. A design taken out through EDIF and read back therefore names that part CONN4 in library gateway, which is the same fact with the redundancy gone.

.eds is refused rather than treated as EDIF. It is a dual-capability format, a netlist plus the faithful schematic geometry beside it, and there is no schematic writer yet, so emitting one from the netlist writer alone would produce a file claiming to carry a drawing that carries none.

version

Print this build’s identity: the version, the commit it was built from, and the Go toolchain and platform. agni --version prints just the first line.

This is the same string a results document records as its producer, so a report archived months ago and the build in front of you can be compared directly. Worth capturing alongside any report you keep.

agni v0.1.1
  built:    2026-08-10T14:46:21Z
  go:       go1.26.4
  platform: darwin/arm64

A build from a source clone reports the commit rather than a release (b020fea02643, suffixed +dirty when the tree had uncommitted changes), because that is what it honestly is.

Global flags

flag what it does
--symbol-path <dir> directory to search for symbol files (.sym for xschem/gEDA, .kicad_sym for KiCad) so schematics that name rather than embed their symbols resolve to pin-level nets and faithful artwork. Repeatable, searched recursively, and the schematic’s own directory is always searched. Defaults to AGNI_SYMBOL_PATH (colon-separated) when the flag is absent, which is how the container image supplies its bundled libraries to every subcommand.
--mount <name>=<path> expose a folder as a named mount (repeatable). Every command takes it, not just serve. An artifact is addressed as mount://<mount>/<path>, and with a matching --mount the CLI and a server produce identical URIs for the same design, so a review created either way records the same design and the two are directly comparable. Without it the CLI mints a mount per argument, rooted at the enclosing project when there is one, so agni check some/board.edn still works with no configuration. You may also pass a full mount://... URI in place of a path.
--as-named read exactly the file named, even when its design.yaml declares it a companion view of a different entry. Without it, analysing a declared companion (a schematic export, a board) reads the design’s entry instead, because a companion is a view of the design rather than a second source of it. Reach for this when reading a view as a netlist is the point, such as checking that two views of one design still agree.
--version print the build version and exit.

Advanced and developer commands

agni also has native (render/open with the design’s own EDA tool), validate (reader-health smoke over many files), census, and derive (datasheet extraction). validate is worth knowing as a user: point it at a folder of exports and it reports which of them this tool can actually read. The rest sit closer to the engine and are covered in the developer docs.

Which formats are read

The reader is picked by file extension (case-insensitive): EDIF netlists (.edn/.edf/.edif) and geometry (.eds), KiCad (.kicad_sch/.kicad_pcb/.kicad_pro), IPC-2581 (.xml/.cvg), Telesis netlists (.tel, netlist only, no geometry), and .sch (xschem / gEDA / legacy KiCad, sniffed by header).

A schematic that references external symbols resolves them by one of three routes, in the order they are searched: its own sym-lib-table for a KiCad project, the symbols library its design descriptor declares (defaulting to symbols beside the descriptor), and any --symbol-path directory. A design that belongs to a project needs no flag, and the flag stays the escape hatch for a library the project does not know about.