# Improve CLI API Error Display Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Preserve useful server error details in Fabro CLI output without adding a new error taxonomy or sprawling command-specific handling.

**Architecture:** Keep error flow centered on existing `anyhow`, `source()`, `miette`, `TaggedFailure`, `ApiFailure`, `api_failure_for`, `classify_api_error`, and `raw_response_failure_error` abstractions. Preserve response details centrally in `fabro-client`, then add sparse user-action context at command boundaries.

**Tech Stack:** Rust, `anyhow`, `miette`, `progenitor_client`, `httpmock`, `cargo nextest`.

---

## Summary

Improve failed `fabro run` and related API command output by preserving server response details through the existing error chain. The target display for run creation failures is:

```text
x could not create run
  caused by: missing field `dirty` at line 1 column 2834
```

For non-JSON plain-text bodies, the CLI should retain the body instead of collapsing to status only:

```text
x could not create run
  caused by: request failed with status 422 Unprocessable Entity: Failed to deserialize ...
```

## Key Changes

- [x] In `lib/crates/fabro-client/src/error.rs`, keep `map_api_error` synchronous. Do not make it read `UnexpectedResponse` bodies, because that requires async body consumption.
- [x] In async client paths that receive `progenitor_client::Error` after `.await`, use `classify_api_error(err).await` so `UnexpectedResponse` bodies are consumed and preserved.
- [x] Update the token-refresh retry path in `Client::send_api` so retry failures also go through the async classifier instead of `.map_err(map_api_error)`.
- [x] Update optional fetch paths that currently call `map_api_error` after `.await` (`get_run_logs`, `read_run_blob`) to use `classify_api_error(err).await.error`, preserving existing `is_not_found_error` behavior through `ApiFailure`.
- [x] In `lib/crates/fabro-cli/src/commands/run/create.rs`, wrap `client.create_run_from_manifest(built.manifest).await` with `context("could not create run")`.
- [x] Do not add new public error types, new CLI diagnostic enums, run-specific API error branches, or a new version-compatibility framework.
- [x] Do not change server wire behavior in this patch.

## Interface Impact

- No public Rust API additions.
- No OpenAPI/schema changes.
- CLI stderr output changes for failed API calls by showing action context plus the existing source chain.
- Exit codes remain unchanged because `ApiFailure` and existing `ExitClass` tagging stay in place.
- `--json` behavior remains unchanged; do not add new JSON error payloads.

## Test Plan

- [x] Add or adjust `fabro-client` unit tests in `lib/crates/fabro-client/src/error.rs` for structured JSON API errors:
  - `errors[0].detail` remains the displayed message.
  - `errors[0].code` remains discoverable through `api_failure_for`.
  - Existing 401 `ExitClass::AuthRequired` behavior still passes.
- [x] Add a `fabro-client` async test for a plain-text `422` `UnexpectedResponse` through `classify_api_error`:
  - The displayed error includes both the status and response body.
  - `api_failure_for` reports status `422`.
- [x] Add a CLI integration test in `lib/crates/fabro-cli/tests/it/cmd/run.rs` with a mock server returning `422` from `POST /api/v1/runs`:
  - `stderr` includes `could not create run`.
  - `stderr` includes the response detail/body, such as `missing field \`dirty\``.
  - `stderr` does not collapse to status-only output.
- [x] Run targeted tests:

```bash
cargo nextest run -p fabro-client
cargo nextest run -p fabro-cli --test it run
```

- [x] Run workspace checks:

```bash
cargo +nightly-2026-04-14 fmt --check --all
cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings
```

## Assumptions

- The first fix should improve error presentation, not add backwards compatibility for the `git.dirty` manifest change.
- Version-skew-specific hints are out of scope unless they can reuse already-available metadata without extra probing or new error plumbing.
- Existing `miette` cause-chain rendering is the display mechanism; implementation should make the error chain better, not bypass it.
