Skip to content

Unify CLI error handling on CliError and the three-line format#134

Merged
JRS1986 merged 1 commit into
mainfrom
refactor/unify-cli-error-handling
Jul 6, 2026
Merged

Unify CLI error handling on CliError and the three-line format#134
JRS1986 merged 1 commit into
mainfrom
refactor/unify-cli-error-handling

Conversation

@JRS1986

@JRS1986 JRS1986 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the error-handling unification flagged in the codebase assessment (and designed but under-adopted since issue #92): CliError + the three-line error / next / see format is now the single failure surface for the CLI.

  • main() catches CliError centrally and renders format_error(...) to stderr, returning exc.exit_code. The special-cased SystemExit dance around _normalize_args_tools_in_place is gone — normalize_tools' CliError just propagates.
  • Domain validation errors raise CliError with recovery hints: session (git missing, not a repo, no active session, no start_commit, git command failures), memory (forbidden/restricted/unknown classes, secret-looking content, promote errors), pilot/doctor (unknown persona), skills (empty name), context (_validate_choice). Previously these were bare ValueError/RuntimeError printed as Error: ... by 7 copy-pasted handler catches — or, for skills/context, raw tracebacks.
  • run_pilot no longer launders CliError into ValueError — the unknown-tool error now keeps its see: wiki link that the conversion was discarding.
  • select-model missing-prompt now uses the same shape (exit code still 2).
  • CliError drops frozen=True: Python's re-raise machinery (contextlib.__exit__) assigns __traceback__, which a frozen dataclass rejects with FrozenInstanceError. This was a latent landmine — any CliError crossing a @contextmanager crashed.
  • Deliberately untouched: team.py and knowledge.py already catch their own raises at the public-function boundary and return warning-based results — converting their internal raises would have broken those catches.

Exit codes are unchanged throughout. Tests asserting RuntimeError/ValueError on the converted paths now assert CliError (same match strings); CHANGELOG documents the library-facing exception-type change under Unreleased.

Example (before → after)

$ coding-scaffold session diff
Error: No active session state file found.
$ coding-scaffold session diff
error: No active session state file found.
  next: Run `coding-scaffold session start` first.

Verification

  • uv run pytest -q: 638 passed in 3.12s.
  • uv run ruff check src tests: clean.
  • Manual smoke test of session/memory/pilot/setup error paths — all render the three-line shape with correct exit codes.

🤖 Generated with Claude Code

main() catches CliError centrally and renders it via format_error, so
every command shares one failure surface. Domain validation errors in
session, memory, pilot, doctor, skills, and context now raise CliError
with a recovery hint instead of bare ValueError/RuntimeError; the seven
per-handler 'Error: {exc}' catch blocks in cli.py are gone, and
run_pilot no longer launders normalize_tools' CliError (with its wiki
link) into a plain ValueError.

CliError also drops frozen=True: contextlib's re-raise assigns
__traceback__, which a frozen dataclass rejects with
FrozenInstanceError.

Exit codes are unchanged; team.py and knowledge.py keep their existing
warning-based results (they already handle their own raises at the
public-function boundary).
@JRS1986
JRS1986 merged commit 673a25c into main Jul 6, 2026
3 checks passed
@JRS1986
JRS1986 deleted the refactor/unify-cli-error-handling branch July 6, 2026 17:14
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.

1 participant