ADR-039: Drive the CLI with Fire: typed functions, derived flags
Status Active · Filed 2026-08-07 · Issue #32
Context
Review on ADR-030 proposed the Fire library, and the analysis
split into a naive shape (wrapping the existing argparse main()s — a
dependency added to keep the dispatch table) and an idiomatic one: delete
the per-module argparse layers and expose typed functions Fire drives
directly. The review chose the idiomatic shape, as a draft PR to be judged
on the diff.
The load-bearing constraints, found up front:
- Exit codes are not return values. Fire prints what a function
returns, so
luria lintreturning 1 would print “1” and exit 0 — a CI gate that green-lights everything. Every command signals failure by raisingSystemExit, and returnsNonealways. - Each module already parsed
sys.argvitself, so nothing could be wrapped: the argparse layer had to come out module by module, replaced by a typedrun()whose signature is the flag surface.
Decision
fire.Fire(COMMANDS) over a dict of eight typed run functions. Flags
and help derive from signatures and docstrings; there is no parsing layer
left to drift from the functions it wraps. Every module keeps a standalone
entry (python -m luria.ref_status --all) through the same functions via
fire.Fire(run) under its __main__ guard, so the vendor-one-file story
survives with Fire as the one convention.
The invocation surface is unchanged. Every spelling CI and the
composite actions use — luria lint, luria link --fix, luria index --check, luria new adr --title X, luria collect --commit, luria init --issue-url U — parses identically under Fire; the end-to-end smoke
(init → index → new → lint) passes byte-for-byte on exit codes.
Alternatives considered
- Keep the hand-rolled dispatcher (ADR-030’s shape) — 40 lines, zero dependencies, tiered help. The default this proposal must beat; the draft PR’s writeup argues the trade in both directions.
- Naive Fire (
fire.FireoverSystemExit-wrapped argparse mains). Adds the dependency without removing the thing it was meant to replace; rejected in the review thread before this draft. - Click/typer instead. The same deletion is available with decorators and explicit registration; Fire was the named proposal, and its derive-from-signature posture is the most aggressive version of the “code is the interface” bet being evaluated.
Consequences
argparseis gone from the package;fire>=0.7joinsPyYAMLas the runtime dependencies.- Help output is Fire’s house format (
NAME/SYNOPSIS/FLAGS), replacing the tiered contributor/CI usage text — the tier distinction now lives only in this file’s docstring and the docs. - An unknown command exits 2 with “Cannot find key” plus the list of available commands — the ADR-030 refusal property, now Fire’s rather than ours.
- A future command author must know one rule the old shape enforced by
type signature: raise
SystemExit, never return a status — Fire prints return values.