CLI --help/--version does not meet POSIX/GNU/clig.dev conventions (hand-rolled parser) #175
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The
auraCLI parses argv entirely by hand (noclap/argh/structoptincrates/aura-cli/Cargo.toml), so nothing supplies the conventional--help/--versionbehaviour for free. A read-only audit against the three relevantconvention layers found the foundation clean (stream/exit discipline, argument
syntax) but the help experience itself well short of standard.
This issue is the self-contained basis for a decoupled session. Line anchors
below are a snapshot at the time of the audit (
mainaroundebebc24) and willdrift — re-locate by symbol: the
USAGE/COST_FLAGS_NOTEconsts and theargs.iter().any(|a| a == "--help" || a == "-h")short-circuit infn main(
crates/aura-cli/src/main.rs), plus the per-subcommandusage()closures.The standard (three layers)
syntax only (
-o value, clustering,--terminator,-= stdin, operandorder). Says almost nothing about help text.
--helpand--versionare required;--help→ stdout, exit 0; options list, description,bug-report footer;
--foo=value; long-option abbreviation.subcommand tool: scoped subcommand help,
tool help <sub>, an Optionssection (one flag per line), SYNOPSIS notation, exit 2 = usage error vs 1 =
runtime failure.
aura is a git/cargo-style subcommand tool, so the third layer is the most
relevant yardstick.
Already compliant — do NOT regress
--help/-h→ stdout, exit 0(
main.rs:4038-4041); every error →aura: {msg}on stderr; results → stdout;never mixed.
-haliases--helpcorrectly (any argv position, overrides other args,suppresses the run).
[optional],<placeholder>,|alternatives).multi-value as one comma token (G8); binary name
aura(G1/G2); per-utilitypositional operands (
run <file.json>,chart <name>).Deviations (verified), by severity
High
--helpis a single dense one-lineusage:blob with no Options section.All ~13 invocation forms crammed on one line joined by
|; no flag gets itsown line + description. Only the three cost flags have prose
(
COST_FLAGS_NOTE). —main.rs:4014-4019(clig.dev help-options-section).--helpcheck (main.rs:4038) firesbefore the subcommand
match(:4042) and always prints the global blob,so
aura sweep --help,aura mc --help, … are byte-identical. (clig.devhelp-subcmd-scoped).
--version/-V. GNU MUST.aura --versionhits the catch-all(
_ =>,:4186) → USAGE on stderr, exit 2. The version exists(
version.workspace = true→0.1.0) but is never surfaced. (GNUsupport-version).
Medium
usage()strings (sweep:1668, walkforward:1790,generalize
:1718— listing all flags) are reachable only on a parseerror (stderr, exit 2), never via
--help. The detail is in the binary; theexplicit help request routes past it. (Exception:
runshares its text withthe global path, incl.
COST_FLAGS_NOTE.)aura help <subcmd>verb —aura help sweepis a usage error.leading synopsis tokens).
--flag=value(GNU equals) form — exact string match;--real=EURUSDerrors.run_dispatcherrors), not just usage errors — and inconsistently:append_familywrite failure → 1 (:1926),TraceStore::writefailure (sameclass) → 2 (
:202).Low
aura(no args) errors (exit 2) instead of concise help.--helppointer".--end-of-options terminator not recognized.-(stdin) not accepted as an operand where stdin is read.usage:prefix inconsistent across subcommands (sweep/walkforward/generalizeomit it).
reproduce <bad-id>→ exit 2 vsruns family <bad-id>→ exit 0 for the same"unknown id".
Highest-leverage direction (for the session to spec, not a fixed plan)
--helphandling after subcommand recognition and print thatsubcommand's
usage()on stdout (exit 0) — the scoped detail texts alreadyexist, they are just wired to the error path. Bare
aura/aura --helpkeepsthe top-level overview.
--version(and-V) arm surfacingCARGO_PKG_VERSION(+ optionalAURA_COMMIT), stdout, exit 0.an Options section per subcommand + a top-level Commands list closes most of the
gap. A crate would need a C16 per-case dependency review.
Constraints / notes
research-side CLI only.
aurafor empirical checks(
target/artifacts only, no source change).the cycle spec, per project discipline.
Cycle discovery — swarm triage (autonomous,
/boss)A synthetic-user swarm (minimalist, power-user, maintainer, skeptic,
off-ledger wildcard) populated the five load-bearing forks this cycle forces.
Convergent forks are decided on cited ground and recorded below; the two
divergent direction forks are routed to the user.
Decided on cited ground
Primary consumer = LLM / automation (all five stances converge). C14
(
docs/design/INDEX.md:1057) names the CLI "the primary surface for the LLMand automation; the human drives the visual face." This frames scope:
affordances that serve the machine consumer (scoped structured help,
--versionprovenance, consistent exit codes) are in; human-only niceties(prose usage examples, long-option abbreviation,
--as-stdin for humans)are deprioritized.
Convergent scope core — all five stances agree the cycle delivers at least:
aura <sub> --helpprints that subcommand's ownusage on stdout, exit 0. The detailed per-subcommand texts already exist in
the binary but are wired only to the parse-error path (stderr, exit 2); the
fix is a near-free re-route (move the
--helpshort-circuit aftersubcommand recognition). Wrinkle the fix must resolve:
run/sweep/mceach carry two disjoint grammars under one subcommand token (a
.json-blueprint branch and the built-in/--strategybranch), so"print that subcommand's usage" must decide which grammar
aura run --helpshows.
--version/-V— a GNU MUST; the version exists (0.1.0,version.workspace = true) but is never surfaced. Also machine-relevant(provenance), so it survives the LLM-first reframe.
Exit-code inconsistency is a real, isolated bug; the full re-partition is
deferred. Four of five stances agree: the same write-failure class exits
inconsistently —
append_familywrite failure → exit 1 (main.rs:1926) vsTraceStore::writefailure → exit 2 (main.rs:202). A genuine defect to fix.The broader "exit 2 = usage error / exit 1 = runtime failure" re-partition
(deviation #8) is NOT folded in:
crates/aura-cli/tests/cli_run.rspinsexit 2 for runtime "no local data" failures (:142/:166/:195/:234), so the
re-partition is a breaking change to the observable/automation contract and to
~46 exit-2 assertions — a deliberate ledger decision, not a refactor.
Test-contract renegotiation is in scope, not free. The do-not-regress
suite already pins current behaviour:
per_subcommand_help_is_uniform_stdout_exit_zero(
cli_run.rs:673, the #131 pin) asserts--helpis uniform acrosssubcommands — in direct tension with the scoped-help goal — and
run_help_surfaces_cost_flag_units_note(:702) pins cost-note substrings anysingle-source change must carry through. Whoever specs this budgets editing
pinned contracts, not just adding new tests.
Routed to the user (direction forks the swarm could not ground)
Fork A — parser architecture: keep the hand-rolled argv parser vs adopt a
parser crate (clap-class). The swarm split with no citable common ground.
Power-user + maintainer: the 5902-line hand-roll (60
usage()sites, growingevery cycle a subcommand is added) is exactly the carry C16
(
INDEX.md:1132-1139, "hand-rolling what a vetted standard crate already doesis the anti-pattern") names; the CLI is research-side, not the frozen deploy
artifact (invariant 8), so the dependency cost is only a dev-loop/compile tax,
and a derive-based parser yields scoped help,
--version,--flag=value, andcompletions from one declaration. Skeptic: a help-text defect is not a parser
defect (the issue itself calls the argument-syntax foundation "exemplary"), so
rewriting a working 5902-line parser is risk with no payoff. Minimalist keeps
hand-rolled but marks the keep-vs-adopt call itself
ungrounded. C16 cuts bothways (it discourages hand-rolling a crate's job, but mandates a deliberate
per-case weigh, no blanket admission), and the blast radius differs radically:
keep = small additive change + rewire; adopt = rewrite of a 5902-line parser +
renegotiation of a 3633-line pinned test surface. A per-case C16 dependency
review the user owns.
Fork B — audience-inversion tension: human clig.dev conformance vs a
machine-first surface. The wildcard surfaced, on cited ground (C14
INDEX.md:1057), that #175 measures aura against clig.dev — a human at aterminal — while the ledger ranks that audience second. The reframe: rather
than furnish a human
--helpexperience, invest in a machine-first surface —structured/JSON help or a command manifest the LLM consumes (a pattern already
latent in
graph introspect --vocabulary,crates/aura-cli/src/graph_construct.rs),typed/structured errors, and
aura run -reading a generated blueprint fromstdin (today
run/sweep/mcgate the blueprint onis_file(), so anop-script generator must write a temp file first —
main.rs~4055/~4118/~4157,the
rest.first().filter(...)operand gate). A project-direction questionoverlapping the #157/C21 op-script trajectory; only the user can set whether
this cycle stays human-convention-compliance or turns machine-first.
Orchestrator recommendation (for the user to ratify or override)
per-subcommand Options section + a top-level Commands list; treat a
clap migration as its own C16-review refactor cycle if wanted, not huckepack
on a help-text fix. Rationale: the scoped-help core is near-free in the
existing parser, whereas a clap rewrite's payoff (carry reduction) is
decoupled from this cycle's help/version goal and carries a large
test-contract renegotiation.
convergent core + an Options section), and file the machine-first surface
(JSON/manifest help, stdin op-scripts) as a separate forward item on the
#157/C21 track. Rationale: the convergent core already serves the machine
consumer; the reframe is a distinct feature line, not a help-convention fix.
Both recommendations are the orchestrator's; the two forks stay the user's
call until ratified.
Fork decisions ratified by the user (2026-07-01)
Following the swarm triage in the prior comment, the two direction forks are
settled by the user and the cycle is paused on a coordination blocker.
Recorded here so a later resumption picks up from the tracker.
Fork B — human/GNU convention compliance (not the machine-first reframe)
User: "wir bleiben bei gnu-compliance" ("we stay with GNU compliance"),
2026-07-01. The cycle targets human / GNU / clig.dev convention compliance.
The wildcard's machine-first reframe (JSON/manifest help, typed errors,
aura run -stdin op-scripts) is NOT part of this cycle — a distinct featureline to file forward on the #157/C21 op-script track if wanted.
Fork A — adopt a parser crate (clap)
User: "ich tendiere zu clap" ("I lean toward clap"), 2026-07-01, chosen over
the orchestrator's keep-hand-rolled recommendation. Premise correction that
informed the choice: the "5902-line parser" framing (carried into the swarm
prompts, hence its keep-vs-adopt reasoning) was wrong —
main.rsis 5902 linesTOTAL (the whole CLI binary). The actual argument parsing is ~under 1000 lines
(14 small
parse_*fns + the dispatchmatch); the bulk is ~3000 lines ofsim-harness execution (
run_*/emit_*/render_*) plus 1757 lines of inlinetests. clap was chosen knowing the real parsing surface is modest; the
justification is C16 (
INDEX.md:1132-1139, use the vetted standard crate) plusone declarative source yielding scoped help,
--version,--flag=value, andcompletions — resolving the whole convergent core in one restructure rather
than piecemeal additive fixes. The CLI is research-side (invariant 8), so
clap's dependency/build surface is a dev-loop tax, not a frozen-artifact tax;
the per-case C16 review is to be documented in the spec/ledger.
Design points the spec must carry (clap-specific)
run/sweep/mceach carry two disjointgrammars under one token — a
.json-blueprint branch vs thebuilt-in/
--strategybranch, today discriminated byrest.first().filter(|a| a.ends_with(".json") && is_file()). clap-derivedoes not model this natively; it needs a deliberate solution (external
positional + manual post-dispatch, or nested subcommands). Plus the nested
positionals
runs family <id> rank <metric>and thegraph build/graph introspectsub-subcommands.usage errors by default, which makes the clean "2 = usage / 1 = runtime"
partition easier — but that is the re-partition previously deferred as a
contract break against the exit-2-runtime pins in
cli_run.rs(:142/:166/:195/:234). Because clap changes the error surface regardless, the
spec decides whether to take the partition now or preserve the old
exit-2-runtime codes. Orchestrator leaning: take it, since clap breaks the
surface anyway.
text; the exactly-pinned
"usage"stderr substrings and exit codes acrosscli_run.rs(part of the 1757 test lines) must be renegotiated — includingper_subcommand_help_is_uniform_stdout_exit_zero(:673, the #131 pin, indirect tension with the scoped-help goal) and
run_help_surfaces_cost_flag_units_note(:702).Blocker — paused, waiting on the parallel main session
User: "wir warten" ("we wait"), 2026-07-01. A separate session is actively
committing to
main, touchingcrates/aura-cli/src/main.rs(e.g.bd2003e,aura sweep <bp.json> --list-axes). A clap rewrite restructures the wholeparsing layer of that same file, so running it in parallel would collide at
landing time (two large restructures of one block). The cycle is paused until
the parallel session releases
aura-cli. On resumption: branch fresh from thethen-current
main(do NOT build on the stalea78a055worktree base) so theclap restructure captures all current subcommands/flags (incl.
--list-axes)with no post-hoc conflict.
Status: design settled, ready for spec production, blocked on the parallel
main-session releasing
aura-cli.Exit-code fork resolved (2026-07-01)
The one remaining open design point — how to resolve the exit-code overload
(deviation #8) once clap forces the error surface to change — is settled by the
user: "Natürlich A." ("Of course A."), 2026-07-01.
Option A — clean partition: exit 2 = usage error, exit 1 = runtime failure.
The runtime failures that today exit 2 (no local data, I/O, dispatch errors —
cli_run.rs:142/166/195/234) move to exit 1; clap supplies exit 2 for usageerrors natively. The exit-2-runtime test pins are renegotiated as part of the
clap restructure (they must be touched anyway). Chosen over Option B (leave
runtime failures on exit 2, overload preserved) because clap changes the error
surface regardless, making this the moment to heal the semantics rather than
carry the overload through the rewrite.
Note: adopting clap also absorbs several Low-tier deviations for free —
--flag=value(GNU equals form), the--end-of-options terminator, andlong-option abbreviation are native clap behaviour, so they need no separate
spec work.
--as-stdin (a Low deviation) is NOT clap-automatic (it is applogic replacing the
is_file()gate) and belongs to the deferred machine-firsttrack (Fork B), out of this cycle.
All load-bearing forks for this cycle are now settled. Status unchanged: design
settled, ready for spec production, blocked on the parallel main-session
releasing
aura-cli.Design decisions derived at spec time (specify, 2026-07-01)
Spec
docs/specs/0098-cli-clap-gnu-compliance.mdwritten. Beyond theuser-ratified forks (clap / GNU-compliance / exit-code Option A), the spec
resolves three derived design decisions:
grammars under one token (the
.ends_with(".json") && is_file()discriminator, a lockstep quartet at main.rs:4249/4297/4334/4376). Decision:
one clap args struct per such subcommand with an optional
[blueprint]positional + the union of both branches' flags, and a post-parse dispatch on
the same is_file() predicate that selects the branch and validates flag
applicability (unexpected-branch flags → usage error, exit 2). Derived:
preserves the exact current surface; the alternative (renaming into distinct
subcommands, e.g.
run-blueprint) would break the surface and the pinnedtests, contradicting Fork B (convention compliance, not redesign).
clap parse failures + aura's argument-structure validations clap cannot
express (--from/--to without --real, cost flags without an R-harness,
generalize's required knobs, dual-grammar flag applicability). Runtime
failures (exit 1): failures after a valid parse (no local data, no geometry,
no recorded run/family, tap-not-found, name collision, persist write
failure). reproduce-diverged stays exit 1 (a semantic verdict = runtime
class). Derived directly from Option A's semantics.
--version, behaviour-preserving on exit codes (parse errors 2, domain refusals
stay 2, reproduce 1). Iteration 2 = the exit-code split (domain refusals 2→1,
normalize the lone generalize persist inconsistency at main.rs:1924 vs
1878/2268). Derived: the two iterations' test-pin surfaces are largely
disjoint (parser-path usage pins vs domain-refusal exit pins), so the split is
clean and iteration 1 ships the scoped-help + version core reviewably before
the exit-code contract change.
The spec covers the full cycle; iteration 1 (clap migration + scoped help +
version) is the shippable first slice, iteration 2 the exit-code split.
Iteration 1 shipped (clap migration) — 2026-07-01
Iteration 1 of the clap adoption landed (feat(0098), commit
366170aon the cyclebranch): the hand-rolled parser is replaced by a clap derive parser — scoped
<sub> --help,--version/-V, a per-flag Options section,--flag=value, the--terminator, and long-option abbreviation. Exit codes behaviour-preserved(the split is iteration 2). Execution layer untouched; the dual-grammar
subcommands (run/sweep/walkforward/mc) are mapped via an optional
[blueprint]positional + a post-parse is_file() dispatch, each built-in handler re-asserting
a stray-positional guard (pinned by two new E2E tests). Full workspace suite +
clippy green.
Derived decision (orchestrator, post-loop): long-option abbreviation. Spec
acceptance criterion 4 claimed clap absorbs it "for free"; it is actually opt-in
(clap
infer_long_args). Two ratified decisions conflict on this low-tier item —Fork B (GNU compliance; getopt_long abbreviates) vs the LLM/automation-first
weighting (an automation caller never abbreviates; human-only niceties
deprioritized). Resolved to DELIVER, on a cost tiebreaker: enabling it is a
single root attribute that propagates to subcommands (not per-struct work), so
the effort is proportionate and GNU compliance is met exactly as the spec
states. Pinned by a test (
--harn->--harness).Carry-on: error-message casing is now mixed ("Usage:" from clap beside preserved
lowercase "usage:" aura messages) — a deliberate quality-hold (the casings are
pinned by renegotiated/preserved tests); cosmetic, a candidate for the
iteration-2 exit-code-split cleanup.
Iteration 2 (the exit-code split: domain refusals 2->1, normalize the generalize
persist inconsistency) remains open.
Iteration 2 classification decisions (planner, 2026-07-01)
Iteration 2 (the exit-code split) plan written (0099). The spec's
"parse-time vs run-time" rule under-specified 6 boundary cases (surfaced by the
post-migration exit-site recon); resolved via a sharper derived principle:
Attribution principle: exit 2 (usage) = something about the COMMAND-LINE
arguments is wrong or inapplicable — including the content of an argv-named file
(the blueprint path is itself an argument). exit 1 (runtime) = the command line
was well-formed but the ENVIRONMENT / recorded state it needs is missing, or
piped STDIN data is bad.
Boundary resolutions:
(the file is an argument; the malformed/open-blueprint pins stay Some(2)).
Only STDIN op-script content + stdin-read I/O (graph build/introspect) ->
runtime 1.
graph introspect --node <unknown>-> usage 2 (invalid argv flag value).blueprint, macd + --real) -> usage 2 (the command is inapplicable; the user
fixes the invocation).
--axisname -> usage 2 (argument fault); a genuinemid-run compute fault (window-too-short) -> runtime 1.
--metric/ rank-metric value -> usage 2 (invalid argv value); apost-run aggregate failure -> runtime 1.
Flips 2->1 (runtime, the spec's explicit list): no local data, no recorded
geometry, no recorded run/family, "run has no tap named", trace-name collision,
family/trace persist-write failures (normalizing the lone generalize
inconsistency -- all persist -> 1 together), missing/corrupt content-addressed
store state (the reproduce path), chart-read failures. reproduce-diverged stays
unchanged); 3 hidden match/guard arms fixed; a partition property test added.
Casing normalization ("Usage:"/"usage:"/bare) deliberately deferred -- cosmetic,
high pin-churn, no functional gain; left for a future polish issue.
Cycle closed — drift-clean (audit 0099), 2026-07-01
Both iterations of the clap adoption shipped and the cycle-close audit is
drift-clean:
366170a): scopedaura <sub> --help,--version/-V, a per-flag Options section,--flag=value/--/long-option abbreviation.
fa42bf3): the usage=2 / runtime=1 partition(attribution principle), the lone generalize persist-inconsistency normalized.
27f0f8e): architect drift-clean — invariant 8 / C16 engine-domainseparation preserved (clap confined to
aura-cli, a leaf binary the frozenartifact cannot pick up); C14 automation-face served, execution layer
untouched. The exit-code partition + clap admission were lifted into the C14
ledger realization note before the ephemeral spec was removed.
Deferred forward: #179 (error-message casing normalization,
idea); themachine-first help surface stays on the #157/C21 track (Fork B settled this
cycle as human/GNU convention compliance).
The six commits sit on the
worktree-cli-helpbranch, unpushed (the/bossrundoes not push autonomously). This issue closes automatically on push/merge (the
audit commit carries
closes #175); until then it stays open pending review.