Skip to content

feat(cli): answer a refused command line with a usage exit code - #399

Merged
Reefact merged 1 commit into
mainfrom
claude/anyarray-distinct-nameof-8m5won
Jul 31, 2026
Merged

feat(cli): answer a refused command line with a usage exit code#399
Reefact merged 1 commit into
mainfrom
claude/anyarray-distinct-nameof-8m5won

Conversation

@Reefact

@Reefact Reefact commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the follow-up ADR-0067 left open. A command line the parser refused exited -1 — a value in no exit-code table, chosen by Spectre.Console.Cli rather than by this repository — and wrote nothing to either stream, so a mistyped command produced no code a pipeline could read and no text a human could.

It now exits 64 with a diagnostic on standard error.

Before and after, on the built binary:

$ fce catalog frobnicate
  before → exit 255 (-1), stdout 0 bytes, stderr 0 bytes
  after  → exit 64
           error: Unknown command 'frobnicate'.
           Run 'fce --help' to see the available commands.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

Why 64. EX_USAGE, the conventional value for a command-line usage error. 2 — the other convention for it — is unavailable because catalog diff already owns it for "changes detected"; taking it would be exactly the collision ADR-0067 exists to prevent. The set is now 0, 1, 2, 64, 130, each with provenance rather than a sequence number.

Where the boundary falls. 64 means the invocation is wrong and a retry will never fix it; 1 means the tool ran and could not finish. The line is the parser's:

Input Code Why
fce frobnicate 64 refused before any command is constructed
fce catalog frobnicate 64 same, inside a branch
fce generate --solution 64 option declared, no value given
fce catalog diff --fail-on bogus 1 the command validates this itself and rejects it

A testable seam. The command tree moves from Program.cs into CliApplication. A top-level program's statements are reachable only by launching a process, and these codes are contract — the parse path had no test because no command's own tests can reach it (it fails before any command exists). Program.cs is now one line.

Tests — six, in CliApplicationExitCodeTests. One pins the regression itself: a refused command line never again exits -1 silently.

Docs — the published exit-code tables in the catalog-versioning reference gain the code, English and French. The CI guide needed no change: it propagates the code without enumerating the set, so nothing there was made stale.

Deliberately out of scope: lenient option parsing. An unknown option such as --nope is still tolerated by the parser rather than refused, so it does not reach 64. Making it a usage error means UseStrictParsing(), which would break every caller passing an extra argument today — a separate decision with its own compatibility cost, not something to slip into this change.

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings, 0 errors
  • dotnet test FirstClassErrors.sln — 2199 passed, 0 failed, 0 skipped across the 13 suites (FirstClassErrors.Cli.UnitTests 64 → 70)
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — 132 passed, run as part of the solution test above

The before/after behaviour quoted above was observed on the built binary, both times.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

User-facing behaviour changed, so both language versions of CatalogVersioningReference move together. The French twin is the one listed by that checkbox in spirit — the file here is CatalogVersioningReference.fr.md, updated in the same commit as its English original.

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-____
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

This implements ADR-0067's follow-up rather than deciding anything new — that record deliberately left the value open as specification. ADR-0067 is accepted and is not edited here: it is a historical log, and its Follow-up Actions section stays as written even though this closes one of them.

Worth noting for the record: ADR-0067's Risks section says the published set and the tool's actual behaviour disagree until this lands. After this, they agree.

Related issues

None. Follows #395, #396 and #397.


Generated by Claude Code

@Reefact
Reefact force-pushed the claude/anyarray-distinct-nameof-8m5won branch 2 times, most recently from f90e2c9 to 2a84ec7 Compare July 31, 2026 08:58
@Reefact
Reefact enabled auto-merge July 31, 2026 08:59
@Reefact
Reefact disabled auto-merge July 31, 2026 09:01
The follow-up ADR-0067 left open. Until now a command line the parser
refused exited -1 — a value in no exit-code table, chosen by
Spectre.Console.Cli rather than by this repository — and wrote nothing to
either stream, so a mistyped command produced no code a pipeline could
read and no text a human could.

It now exits 64 with a diagnostic on standard error naming what was
refused and pointing at --help. 64 is EX_USAGE, the conventional value
for a command-line usage error; 2, the other convention, is unavailable
because catalog diff already owns it for "changes detected" — the
collision ADR-0067 exists to prevent.

The code separates what a retry cannot fix from what it might: 64 means
the invocation is wrong, 1 means the tool ran and could not finish. The
boundary is the parser's. An unknown command, a branch subcommand that
does not exist, an option given without its value are refused before any
command is constructed and answer 64; a value a command validates itself,
such as --fail-on bogus, is that command's own rejection and stays 1.

The command tree moves from Program.cs into CliApplication so the parse
path can be tested at all: a top-level program's statements are reachable
only by launching a process, and these codes are contract. Six tests
cover it, one of them pinning the regression itself — a refused command
line never again exits -1 silently.

The published tables in the catalog-versioning reference gain the code,
English and French. The CI guide needed no change: it propagates the code
without enumerating the set.

Lenient option parsing is untouched. An unknown OPTION such as --nope is
still tolerated by the parser rather than refused, so it does not reach
64; making it a usage error means UseStrictParsing, which would break
every caller passing an extra argument today. That is a separate
decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuSBsGG7wMPMnSeTHKZAom
@Reefact
Reefact force-pushed the claude/anyarray-distinct-nameof-8m5won branch from 2a84ec7 to db47590 Compare July 31, 2026 09:05
@Reefact
Reefact enabled auto-merge July 31, 2026 09:06
@Reefact
Reefact merged commit 0ab0746 into main Jul 31, 2026
29 checks passed
@Reefact
Reefact deleted the claude/anyarray-distinct-nameof-8m5won branch July 31, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants