Library M8: the CLI as a library¶
M8 is the eighth of the eleven milestones of #48, which turns Henad into a library published on crates.io. henad-cli gains
src/lib.rswithCliOptions,SOME_RUNS_NOT_OKandrun(options, arguments) -> u8, and thehenad-clibinary is three lines overrunwithexample_models(). henad-models is an optional dependency behind the defaultexample-modelsfeature, which the binary requires, andcargo build -p henad-cli --no-default-featuresbuilds the library alone. The command line keeps every output byte for byte: the golden tests, every henad-cli test,--help,--version, the usage and run errors with their exit codes, and the--jsonlines of five benchmarks apart from their timings. The interleaved comparison against the M7 binary passed on all four configurations.
State before¶
48-library stood at 654539e, M7's commit, with a clean tree.
henad-cli had a binary target alone, src/main.rs with the crate doc, Args, Mode, main, the benchmark and the exports, beside the private explore and json_report modules.
main installed the panic hook, parsed through clap's derived Parser::parse under the fixed name henad-cli and henad-cli's own version, built example_models(), and returned anyhow::Result<ExitCode>.
explore.rs held SOME_RUNS_NOT_OK, built the sweep's Provenance from build_info!() and std::env::args_os(), and returned ExitCode.
The info line's engine_version read henad-cli's CARGO_PKG_VERSION.
existing_invocations_keep_their_mode read the command lines of main.rs and docs/reference/cli.md, with a floor of 12 documented lines that are not sweeps.
What was done¶
The library¶
src/main.rs moved to src/lib.rs with git mv, and the binary is a new main.rs.
The rest of [4.5.2] follows.
CliOptionsholds theModelSet, the host'sBuildInfoand the command name.CliOptions::new(models, host)takes the name fromhost.package(), andcommand_namesets another. It derivesDebugandClone.SOME_RUNS_NOT_OKmoved fromexplore.rsto the crate root as the publicu83.run(options, arguments) -> u8installs the panic hook, collects the arguments, and parses them throughArgs::command()withCommand::nameandCommand::versionfrom the options, thenfrom_arg_matches_mut, asParser::try_parse_fromdoes. A clap error prints throughError::printand returns its own code, 0 for--helpand--versionand 2 for a usage error, which is whatError::exitdid. Any other error prints asError: {error:?}and returns 1, as theanyhowmaindid.rundestructures the options it takes by value. The signature is the design's, and clippy'sneedless_pass_by_valuerefused a body that only borrowed them.- The old
mainbody is the privaterun_args(models, host, args, arguments) -> Result<u8>, with the options' set in place ofexample_models(). A sweep'sProvenanceis built there from the host'sBuildInfoand the argumentsrunwas given, andexplore::runtakes it. explore::run,merge_shardsandexit_statusreturnu8.median_oflost itspub, since it would otherwise be public API.json_report::inforeadsengine_versionfromhenad_explore::ENGINE_BUILD.version().- The derived
#[command(name = "henad-cli", version, about)]is#[command(about)], and the name and version come from the options at run time. clap'sstringfeature is on in henad-cli, forCommand::namefrom aString. - The crate doc in
lib.rsdescribes the library, with ano_runexample of a host over its ownModelSet, and keeps the ten documented command lines.
Argument parsing, the modes, Reporter, json_report and the text formats stay private.
The binary and the manifest¶
main.rs is three lines over run, with example_models() and the binary's build_info!(), and wraps the code in ExitCode::from.
A binary and a library of one package share the build script's cargo:rustc-env lines, so the stamp build_info!() reads in main.rs is the one M7 set up.
Cargo.toml gains:
[lib], andrequired-features = ["example-models"]on the[[bin]].[features]withdefault = ["example-models"]andexample-models = ["dep:henad-models"].- henad-models as an optional normal dependency, and as a dev-dependency for the unit tests, which build
example_models()with or without the feature. AGENTS.md names the new dev-dependency edge, as its dependency rule asks. [[test]]entries forgoldenandmanifestwithrequired-features = ["example-models"], since both run the binary throughCARGO_BIN_EXE_henad-cli.
Tests¶
- Every henad-cli test now runs through the library, and the two integration tests and the golden outputs through the binary and so through
run. All pass unchanged apart from theu8statuses and the provenance argument. existing_invocations_keep_their_modereadslib.rs, which holds the crate doc, and its floor is 13, the current count of documented lines that are not sweeps: 17 lines, 4 of them sweeps. The new docs section first broke the test twice, once with a sentence opening "henad-cli is a library" and once with a TOML line openinghenad-cli = {. The scanner reads both as command lines, and the page now words them otherwise.- New:
run_returns_the_exit_code_of_each_failure, which runsrunover an emptyModelSetand checks 2 for an unknown flag and for--mergewithout--out, and 1 for a model the set lacks. No other test callsrunin process.
Checks¶
cargo build -p henad-cli --no-default-featuresbuildslibhenad_cli.rliband no binary.cargo tree -p henad-cli --no-default-features -e normal -i henad-modelsprints nothing on stdout. Cargo's "nothing to print" note goes to stderr.cargo clippy -p henad-cli --all-targetswith-D warnings -W clippy::allpasses with all features and with none.scripts/bench_matrix.py --dry-run --binaryprinted the same 437 lines through the M8 binary as through the M7 binary.
Equivalence and the instrument gate¶
The baseline is a release henad-cli built from a git archive of 654539e into a target directory of its own, with the same toolchain.
- The
--jsonlines of five benchmarks matched the M7 binary's apart from the timing fields andengine_version, which read 0.2.0 on both: SIR at 128² with--seed 42and outbreaks at ticks 0 and 60, SIR at 128² without--seed, boids at 2,000 agents with and without--seed, and gpu_sir with--seedand--info, whoseruntimeline matched too. - stdout, stderr and the exit code matched byte for byte for
--help,--version(henad-cli 0.2.0),--bogus(2),sir --merge x(2), an unknown model (1),--list(0) andsir --set grid_width=0(1).
The instrument gate ran the M7 binary's benchmark against M8's, both release builds on rustc 1.97.1 (8bab26f4f 2026-07-14), --json --seed 1 with 1000 steps after a 200-step warm-up and five repetitions a run.
Each configuration ran ten rounds of M7, M8, M8, M7 after a 120 s idle start and 30 s between configurations, and each M8 run paired repetition by repetition with the M7 run beside it, 100 pairs a configuration.
Nothing else was built or run during the timings, and the machine's load average was about 3 from the desktop.
No configuration came near the 1000 s cap. The longest, boids at 1,000 agents, took 31 s.
| Configuration | Repetitions × runs | M7 median | M8 median | Median ratio |
|---|---|---|---|---|
| game_of_life 64² | 5 × 20 | 1.62 ms | 1.63 ms | 1.001 |
| game_of_life 1024² | 5 × 20 | 39.7 ms | 37.9 ms | 0.978 |
| sir 64² | 5 × 20 | 2.04 ms | 1.94 ms | 0.984 |
| boids 1,000 | 5 × 20 | 133 ms | 120 ms | 0.911, rerun 0.905 |
Every configuration passes the bound of 1.05.
boids at 1,000 agents ran 9% faster under M8 on both runs. The timed loop is run_benchmark in henad-explore in both binaries, and M6 saw the same rung run 14% faster than M5 with no explanation. It is flagged as surprising, not read as a finding. A difference in code layout between the two links is one guess, and nothing here tests it.
Docs¶
reference/cli.mdgains Hosting the command line: the official binary included whole fromcrates/henad-cli/src/main.rs, whatrunreturns, whatCliOptionssets, thatengine_versionkeeps reporting Henad's version, theexample-modelsfeature with adefault-features = falsedependency, and thatrunowns its process. The page's opening links to it.- henad-cli's README says the crate is also a library.
- AGENTS.md: the diagram line, the dependency rule (henad-models behind the feature, and the new dev-dependency edge), and the henad-cli paragraph's opening, device sizing, lookup, host build,
engine_versionand the invocation test. - CHANGELOG: two Added entries (the library, the feature) and two Changed entries (the binary over
run,engine_versionfromENGINE_BUILD). None is marked "Breaking:". henad-cli had no library and no features before, the binary prints, writes and exits as it did, andengine_versionreads the same 0.2.0 for the official binary.
Edited tree¶
.
├── AGENTS.md ~ henad-cli as a library, the feature, the dependency rule
├── CHANGELOG.md ~ M8's Added and Changed
├── zensical.toml ~ nav entry #37
├── crates/henad-cli/
│ ├── Cargo.toml ~ [lib], example-models, required-features, henad-models optional and dev, clap's string
│ ├── README.md ~ the library
│ └── src/
│ ├── lib.rs ~ moved from main.rs: crate doc, CliOptions, SOME_RUNS_NOT_OK, run, run_args
│ ├── main.rs + the three-line official binary
│ ├── explore.rs ~ u8 statuses, provenance passed in
│ └── json_report.rs ~ engine_version from ENGINE_BUILD
└── docs/
├── reference/cli.md ~ Hosting the command line
└── developing/agent-record/20261002-37-library-cli.md +
State after¶
M8 is implemented and committed on 48-library on top of 654539e, at the maintainer's request after the hand-over.
The workspace version stays 0.2.0.
HENAD_REQUIRE_GPU=1 ./check.shpasses: 1040 tests, none failed, with the wasm32 typecheck, the packaging, cargo-deny and docs steps and the web build. M7 passed at 1038. The two new tests arerun_returns_the_exit_code_of_each_failureand the crate doc'sno_runexample.uv run --locked zensical buildpasses with no issues, and the hosting section renders withmain.rsincluded.schema_hashes_are_unchanged_since_0_2_0and the golden CLI tests pass unchanged.- The gate ran after the code was final, and only docs changed after the M8 binary was built.
Proposed commit message: feat: CLI as a library
Issues found & future directions¶
- Deviations from [4.5.2].
CliOptionsderivesDebugandClone, which the design leaves open.runreturns clap's own code for--help,--versionand a usage error, 0 or 2, which the design does not state and the binary returned before. henad-cli enables clap'sstringfeature. - The command name sets clap's name, not its binary name. The usage line takes its program name from
argv[0], as before, and--versionprints the configured name. A Python console script then shows its own script name in the usage line. runinstalls the panic hook on every call, which replaces a hook the host set. The design's note saysrunowns the process, and the crate doc andreference/cli.mdsay so.- No CI line builds henad-cli without default features. The two checks above ran by hand. The
featuresjob M10b adds is the natural home for both. - The worktree. This session ran in
.claude/worktrees/m8, a linked worktree on the branch48-library-m8, since a background job edits outside the shared checkout. The commit was made there,48-libraryfast-forwarded to it, and the worktree and its branch were then removed. - Next. M9 (the app as a library) depends on M4, M6 and M7 and can start. M10b's facade depends on M8.