Rich-backed CLI output with --json across every list command
Replaces the hand-rolled ANSI styler (`output.py`) with `rich` for both
human-readable tables and machine-readable JSON, and adds consistent
`--json` support to every list-style command (`pm pool ls`, `pm project
ls/status`, `pm repo ls/pull`, `pm check`, `pm stacker config --list`,
`pm pool add`, `pm pool gc-ops`, the `wt attach/detach/create/delete`
family, `pm project delete --dry-run`). Grouped commands render a
single aligned table with a leading group column whose value appears
only on the first row of each group; horizontal rules under the header
and between groups make the structure visible at a glance.
New `project_manager/render.py` is the single output layer:
- `Column[T]` — per-cell value accessor + optional per-row styling.
Accepts attr names or callables for value and style so domain logic
(dirty→red, Kind→per-status colors, `ahead 3` derivation) lives in
exactly one place colocated with the dataclass.
- `Section[T]` + `GroupColumn` — grouped output drives a single table
with the group value prepended; only the first row of each group
shows the label.
- `JsonShape` — `{group_key: title, items_key: [raw_row, ...]}` layout
so machine consumers get structured data (ints stay ints, enums
become `.value`, paths become strings) instead of parsing display
strings like `"ahead 3"`.
- `__pm_json__` protocol lets types with nested Finding/Path/enum
structure (`Finding`, `StatusRow`, `DetachPlan`, `DeletePlan`)
override the default dataclass serialization.
- `emit_rows` / `emit_sections` / `emit_markup` / `emit_json` are the
only public emit primitives — CLI files shrink to one-line dispatch.
Domain modules carry their own `COLUMNS` spec next to the dataclass
(pool/project/repo/check/attach/detach/create/pull/config). This
removes the 38 hand-rolled `print(f"{row.x}\t{row.y}...")` call sites
and centralizes column titles, value derivations, and styling.
The stacker tree keeps its bespoke composition logic — `fmt.style()`
now emits rich markup instead of raw ANSI, auto-escaping leaf text so
literal tokens like `[LOCAL]` / `[MERGED]` and PR URLs stay literal.
Merged-row dim/strike wrapping constructs markup directly (it wraps
pre-styled fragments; escaping them would break the inner tags).
`output.py` is deleted: rich honors `NO_COLOR` / `FORCE_COLOR` / tty
detection natively via `Console`, so pm's color shim and the
`CLICOLOR_FORCE` hook are no longer needed. `FORCE_COLOR=1 pm pool ls
| cat` replaces the old `CLICOLOR_FORCE=1` path. The `--color-mode`
option on `pm stacker ls` keeps its user-facing semantics but no
longer auto-clamps to `off` on non-TTY — rich strips markup anyway.
Tests: new `tests/test_render.py` covers `emit_rows` /
`emit_sections` / `emit_markup` / JSON / enum+Path coercion / empty
substitution in both pipe and forced-TTY modes. Stacker `ls` color
assertions move from raw ANSI byte patterns to rich-markup regexes.
The old tab-separated surface for `pm pool ls` / `pm project ls/status`
/ `pm repo ls` / `pm check` is intentionally broken — consumers
should use `--json`.
Promotes `rich>=14,<15` from transitive (via cyclopts) to direct dep.