---
title: "refactor: CommandContext alignment at CLI command boundaries"
type: refactor
status: completed
date: 2026-04-23
deepened: 2026-04-23
---

# CommandContext Alignment At CLI Command Boundaries

## Overview

Finish the partially landed `CommandContext` refactor in `fabro-cli` by
making `CommandContext` the shared command-boundary abstraction for
invocation plumbing in the CLI command families that already depend on
it or immediately reconstruct it. The goal is to stop threading
`&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`
through command entrypoints when that data is already part of the same
invocation context.

This is a follow-on plan to the earlier server-access refactor. The
current codebase already centralizes `cwd`, merged settings, and server
access in `CommandContext`, but command entrypoints still receive raw
plumbing and then rebuild a context locally. This plan aligns the
surface area around the existing struct instead of introducing a second
context or service wrapper.

## Problem Frame

`CommandContext` exists today in
`lib/crates/fabro-cli/src/command_context.rs`, and many commands
already depend on it for `cwd`, `machine_settings`, `user_settings`, and
`server()` access. The refactor is only half-finished:

- the `printer` field and `printer()` accessor are both marked
  `#[allow(dead_code, reason = "...still being wired through")]`
- `main.rs` still passes raw plumbing into most command families
- many command entrypoints still accept some combination of
  `&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`
- those same commands often construct `CommandContext` immediately
  inside the handler

That leaves the CLI with two overlapping models:

- `CommandContext` as the intended abstraction for shared invocation
  state
- raw parameter threading as the de facto command API

The result is more churn on command signatures, more repeated output
format and JSON-gating branches, and a dead-code-marked `printer` field
that signals the abstraction boundary is unfinished.

The refactor needs to finish in a way that preserves current CLI
behavior and avoids turning `CommandContext` into a new god object.

## Requirements Trace

- **R1.** In-scope CLI command entrypoints use `CommandContext` as the
  shared invocation-plumbing abstraction instead of separately receiving
  `&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`.
- **R2.** `CommandContext` remains narrow: it carries invocation-scoped
  plumbing and shared derived state, not command-specific args, styles,
  or workflow-specific data.
- **R3.** JSON output, verbosity behavior, printer routing, and global
  `--json` restrictions remain behaviorally unchanged for existing
  commands, including `auth status` remaining explicit-global-`--json`
  only rather than switching to persisted `cli.output.format = "json"`.
- **R4.** Target-based and storage-dir-based server resolution semantics
  remain unchanged, including `CommandContext::server()` behavior and
  `ServerSummaryLookup::from_client(...)` usage.
- **R5.** `main.rs` keeps its current pre-tracing bootstrap ordering for
  logging and upgrade checks; `CommandContext` begins after that phase.
- **R6.** The current dead-code allowance on the `printer` field/accessor
  is removed by making printer access a real part of the abstraction, or
  by deleting redundant surface area if a narrower accessor is better.
- **R7.** Representative unit and integration coverage exists for the
  context API, JSON/text output behavior, global `--json` gating, and
  target/connection-based command families touched by the refactor.

## Scope Boundaries

- **In scope:** command families in `fabro-cli` that already use
  `CommandContext` directly or immediately construct one from raw
  plumbing:
  `run`, `runs`, `artifact`, `store dump`, `preflight`, `validate`,
  `graph`, `model`, `secret`, `pr`, `repo`, `auth`, `provider`,
  `config`, `version`, `doctor`, `system`, and the public
  `sandbox preview` / `sandbox ssh` command boundary that currently
  forwards into `commands/run/*` leaf modules.
- **In scope:** top-level dispatch cleanup in
  `lib/crates/fabro-cli/src/main.rs` and family dispatch modules where
  the only reason raw CLI plumbing is still passed down is command
  signature inertia.
- **Out of scope:** `exec`, `server`, `install`, `upgrade`, `workflow`,
  `parse`, `render_graph`, hidden analytics/panic upload commands,
  hidden `run worker`, and `sandbox cp`, except for thin compile-only
  adapters if needed.
- **Out of scope:** changing CLI text, response payloads, exit-code
  semantics, or server connection behavior beyond what is required to
  move the plumbing boundary.
- **Out of scope:** moving `Styles` ownership into `CommandContext` or
  making low-level pure rendering helpers context-aware when explicit
  `Printer` or `Styles` parameters remain clearer.
- **Out of scope:** redesigning `server_client.rs` connection semantics.
  This plan consumes the current `CommandContext::server()` model rather
  than reopening the earlier server-access design.

## Context & Research

### Relevant Code and Patterns

- `lib/crates/fabro-cli/src/command_context.rs` — current
  `CommandContext`, constructors, `user_settings()` / `machine_settings()`
  accessors, and dead-code-marked printer storage.
- `lib/crates/fabro-cli/src/main.rs` — top-level command dispatch still
  passing raw plumbing into most families after bootstrap.
- `lib/crates/fabro-cli/src/server_client.rs` — current target and
  storage-dir connection behavior that must stay unchanged.
- `lib/crates/fabro-cli/src/commands/run/create.rs` — existing example
  of a helper that already takes `&CommandContext`.
- `lib/crates/fabro-cli/src/commands/run/mod.rs` — current mixed model:
  some subcommands build a `CommandContext`, some still read raw
  `cli.output.format`, and `process_local_json` is already vestigial at
  this boundary.
- `lib/crates/fabro-cli/src/commands/secret/mod.rs` — good family-level
  pattern where a single derived server/client can be shared across
  subcommands.
- `lib/crates/fabro-cli/src/commands/pr/mod.rs` — representative mixed
  local/server family using both `CommandContext::base(...)` and
  `CommandContext::for_target(...)`.
- `lib/crates/fabro-cli/src/commands/auth/mod.rs` and
  `lib/crates/fabro-cli/src/commands/provider/mod.rs` — the clearest
  examples of raw `process_local_json` still being threaded despite the
  rest of the state already belonging to the invocation.
- `docs-internal/testing-strategy.md` — CLI integration tests should
  stay command-driven and black-box, with implementation-facing behavior
  covered by unit tests near the code.
- `lib/crates/fabro-cli/src/commands/sandbox/mod.rs` — the real public
  boundary for preview/ssh command dispatch; leaf implementation lives in
  `commands/run/preview.rs` and `commands/run/ssh.rs`, but the command
  family boundary is `sandbox`.
- `lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs` and
  `lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs` — existing
  command-owned coverage for those public entrypoints.

### Current-State Inventory

- `CommandContext::{base, for_target, for_connection}` currently has
  **42** call sites across **37** command files under
  `lib/crates/fabro-cli/src/commands`.
- The command tree contains **one** existing helper that already accepts
  `&CommandContext` directly:
  `lib/crates/fabro-cli/src/commands/run/create.rs`.
- The `printer()` accessor currently has no call sites, which is why the
  dead-code allowance still exists.
- For in-scope command files, the dominant remaining use of raw
  `&CliNamespace` is reading `cli.output.format` or
  `cli.output.verbosity`; that is a signal that the data belongs on the
  shared invocation context rather than each command signature.
- `process_local_json` is now concentrated in `auth`, `provider`,
  `graph`, `sandbox preview`, and `sandbox ssh`. That makes it a good
  candidate for a dedicated invocation-context field/helper instead of
  continued parameter threading.

### Related Context

- `docs/plans/2026-04-08-cli-services-command-context-refactor-plan.md`
  — earlier plan that introduced the current `CommandContext` and server
  access model. This plan finishes the command-boundary alignment that
  document did not fully land.
- `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
  — recent owner-first context plan that reinforces the repo preference
  for dense, owner-scoped context objects instead of repeated free-form
  plumbing.
- `git log -- lib/crates/fabro-cli/src/command_context.rs` shows recent
  follow-on commits including `simplify: drop duplicate settings plumbing
  from cli/server refactor`, which is consistent with the current goal of
  collapsing overlapping command-boundary APIs.

### Institutional Learnings

- No relevant `docs/solutions/` entries currently cover this seam.

### External References

- No external research used. The repo already has sufficient local
  context, existing partial implementation, and tests for this refactor.

## Key Technical Decisions

- **Use `CommandContext` as the command-boundary API for in-scope
  commands.**
  The abstraction already owns the hard parts: working directory,
  merged settings, and server access. The remaining refactor should move
  entrypoint APIs onto that abstraction instead of continuing to thread
  raw plumbing beside it.

- **Keep `CommandContext` narrow and invocation-scoped, not god-shaped.**
  It should own:
  `printer`, merged CLI-derived settings (`machine_settings`,
  `user_settings`), `cwd`, config-path context, server-derivation state,
  and the invocation-only global `--json` flag.
  It should not own command args, `Styles`, render-only helpers, or
  workflow/build-specific state.

- **Do not store or expose the full `CliNamespace` publicly.**
  Commands in scope only need a small subset of CLI plumbing:
  output format, output verbosity, and global `--json` restrictions.
  Output format and verbosity should come from
  `ctx.user_settings().cli.output`, which already reflects CLI override
  precedence through merged settings. The only truly extra invocation
  field is the global `--json` switch, which is not part of persisted
  settings and therefore belongs on the context explicitly.

- **Preserve the current error-timing split.**
  `CommandContext` construction should keep doing what it does today:
  capture cwd, load local settings, and build merged invocation state.
  Server-target resolution and server-connection failures should remain
  deferred to `ctx.server().await?` or existing explicit
  `resolve_server_target(...)` calls. The refactor should not make
  malformed target/server resolution errors eager at context-construction
  time.

- **Keep `auth status` as an explicit-global-JSON command.**
  Most in-scope commands should read output mode from
  `ctx.user_settings().cli.output`, but `auth status` is a special case:
  it currently emits JSON only for the explicit invocation-wide global
  `--json` path, not merely because resolved CLI settings say
  `output.format = json`. That distinction should remain intact, so the
  context needs both resolved output settings and a separate helper for
  the explicit global JSON flag.

- **Create a base invocation context once, then derive target/connection
  variants from it.**
  `main.rs` should keep using raw settings during pre-tracing bootstrap.
  After that, it should build a base `CommandContext` once for each
  in-scope dispatch path. Command families then derive
  target-based or connection-based contexts from that base without
  re-supplying `Printer` and `CliLayer`.

- **Allow private derivation state inside `CommandContext` if that is the
  simplest way to avoid raw parameter threading.**
  Storing a private `CliLayer` or equivalent internal builder state is
  acceptable if it enables methods like `with_target(...)` or
  `with_connection(...)` and keeps the raw plumbing hidden behind the
  abstraction boundary.

- **Keep render helpers explicit.**
  Command entrypoints and family dispatchers should align on
  `CommandContext`, but low-level helpers such as table rendering,
  summary formatting, and browser-opening routines may keep explicit
  `Printer` / `Styles` / boolean parameters where that stays simpler than
  threading the full context downward.

- **Keep workflow/manifest layer assembly command-local.**
  Commands such as `run`, `preflight`, `validate`, and `graph` should
  continue to build their workflow/project/manifests with the existing
  command-owned helpers. `CommandContext` can provide `cwd`, merged user
  settings for output behavior, and server access, but it should not
  absorb manifest-building policy or workflow-layer composition.

- **Migrate family-by-family with temporary compatibility wrappers if
  needed.**
  This is a cross-cutting refactor with wide signature churn. It is
  better to allow short-lived constructor/adapter overlap during the
  migration than to force a single giant all-or-nothing patch that is
  harder to validate.

## Open Questions

### Resolved During Planning

- **Should this refactor introduce a second wrapper type such as
  `Services` or `InvocationContext`?**
  No. The repo already has a partially landed `CommandContext`, and the
  simplest aligned design is to finish that abstraction rather than
  splitting responsibilities across two overlapping context types.

- **Should `CommandContext` absorb the entire `CliNamespace`?**
  No. Public command consumers should read output mode and verbosity from
  `ctx.user_settings().cli.output`, while command-specific configuration
  stays local to the commands that own it.

- **Should `process_local_json` become part of `CommandContext`?**
  Yes. It is invocation-scoped, currently leaks across multiple command
  signatures, and is the one remaining piece of global command plumbing
  that is not already represented by merged settings.

- **Should preview/ssh be treated as `run` work or `sandbox` work in
  this plan?**
  Treat them as `sandbox` work at the public command boundary. The leaf
  implementation modules remain under `commands/run/*`, but the raw
  plumbing boundary in the current CLI is `commands/sandbox/mod.rs` and
  the plan should align to that boundary and its tests.

- **Should `auth status` remain “explicit global JSON only”?**
  Yes. R3 for this plan is behavioral preservation, and the current
  contract is that `auth status` emits JSON only when the explicit
  invocation-global `--json` switch is active.

- **Should `Styles` move into `CommandContext` in the same pass?**
  No. That would enlarge the abstraction without addressing the actual
  duplicated plumbing problem.

- **Should out-of-scope local commands be forced onto `CommandContext`
  just for uniformity?**
  No. This pass should target the families where `CommandContext` already
  provides real value or is already partially adopted.

### Deferred to Implementation

- **Exact API names for derived contexts.**
  The implementation may settle on `with_target(...)`,
  `for_target_from(...)`, or similar naming. The important contract is
  that callers no longer pass raw `Printer` and `CliLayer` repeatedly.

- **Whether static constructors remain temporarily during migration.**
  If temporary wrappers reduce compile churn while family-by-family
  patches land, they are acceptable. Final cleanup should remove the
  now-redundant raw-plumbing entrypoints from in-scope call sites.

- **Whether `CommandContext` should be cheaply cloneable or should build
  derived variants from private state on demand.**
  Either is acceptable if it preserves the abstraction boundary and does
  not change runtime behavior.

## High-Level Technical Design

> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*

| Boundary | Current shape | Target shape |
|---|---|---|
| `main.rs` -> command family | `dispatch(args, &cli_settings, &cli_layer, process_local_json, printer)` | `dispatch(args, &base_ctx)` |
| family dispatch -> leaf command | raw CLI plumbing plus `CommandContext::for_target(...)` inside the leaf | derive `target_ctx` / `connection_ctx` once from `base_ctx`, then pass `&CommandContext` or already-resolved client/output helpers |
| output mode lookup | `cli.output.format` / `cli.output.verbosity` | `ctx.user_settings().cli.output.*` |
| global `--json` guard | separate `process_local_json` parameter | `ctx` accessor/helper |
| printing | raw `printer` parameter | `ctx.printer()` or a narrower printer extracted from `ctx` |

Directional flow:

```text
bootstrap raw globals/settings for tracing + upgrade check
  -> build base CommandContext once for the in-scope command dispatch
  -> family dispatch receives &base_ctx
  -> family derives:
       target_ctx from target args
       connection_ctx from storage-dir-aware args
       base/local ctx for settings-only commands
  -> leaf command reads:
       ctx.cwd()
       ctx.machine_settings()
       ctx.user_settings().cli.output.*
       ctx.require_no_json_override() or equivalent
       ctx.printer()
       ctx.server().await?
  -> render-only helpers stay explicit over Printer / Styles where simpler
```

## Implementation Units

- [x] **Unit 1: Reframe `CommandContext` as the invocation-boundary object**

**Goal:** Make `CommandContext` capable of carrying the invocation
plumbing that is still leaking through command signatures, while keeping
the type narrowly scoped.

**Requirements:** R1, R2, R3, R4, R6

**Dependencies:** None

**Files:**
- Modify: `lib/crates/fabro-cli/src/command_context.rs`
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Test: `lib/crates/fabro-cli/src/command_context.rs`

**Approach:**
- Add the remaining invocation-only plumbing that does not already exist
  on the context, specifically the global `--json` switch used today via
  `process_local_json`.
- Make printer access part of the live API so the dead-code allowance on
  the `printer` field/accessor can be removed.
- Add context-derivation methods that let callers obtain target-based or
  connection-based variants from a base invocation context without
  re-supplying raw `Printer` and `CliLayer`.
- Keep output format and verbosity sourced from
  `ctx.user_settings().cli.output` rather than storing a second public
  output-format copy on the side, while still exposing the explicit
  invocation-global JSON flag separately for commands like `auth status`
  whose contract is not identical to resolved output format.

**Patterns to follow:**
- `lib/crates/fabro-cli/src/command_context.rs`
- `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
  for owner-first context boundaries

**Test scenarios:**
- Happy path: a base context exposes the same output format and verbosity
  that commands currently read from merged CLI settings.
- Happy path: a base context preserves both resolved output settings and
  the explicit invocation-global JSON flag so commands can distinguish
  between “resolved output format is JSON” and “user passed global
  `--json`”.
- Happy path: deriving a target-based context preserves printer/global
  JSON state and resolves server access through the existing
  `server_client::connect_server_with_settings(...)` path.
- Edge case: deriving a connection-based context with a storage-dir
  override changes only the storage-backed settings path and preserves
  other invocation-scoped data.
- Error path: settings-load failures tied to context construction still
  surface during context construction, while malformed target resolution
  remains deferred until `ctx.server().await?` or explicit
  `resolve_server_target(...)` calls.

**Verification:**
- The `printer` field/accessor is no longer dead code.
- There is one clear way to obtain a base context and derive
  target/connection variants without raw plumbing at the call site.
- The context API makes the distinction between resolved output format
  and explicit global JSON invocation state unambiguous.

- [x] **Unit 2: Move `main.rs` and the run/preflight/graph/sandbox boundary to context-first dispatch**

**Goal:** Eliminate raw plumbing from the top-level dispatch path and the
run-oriented command family plus the public `sandbox preview` /
`sandbox ssh` boundary that already rely heavily on `CommandContext`.

**Requirements:** R1, R3, R4, R5, R6, R7

**Dependencies:** Unit 1

**Files:**
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/command.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/create.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/preview.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/ssh.rs`
- Modify: `lib/crates/fabro-cli/src/commands/sandbox/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/resume.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/rewind.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/fork.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/wait.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/diff.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/logs.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/cp.rs`
- Modify: `lib/crates/fabro-cli/src/commands/preflight.rs`
- Modify: `lib/crates/fabro-cli/src/commands/validate.rs`
- Modify: `lib/crates/fabro-cli/src/commands/graph.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/run.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/create.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/start.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/diff.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/logs.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/preflight.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/validate.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/graph.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`
- Test: `lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs`
- Test: `lib/crates/fabro-cli/tests/it/scenario/recovery.rs`

**Approach:**
- Build a base `CommandContext` in `main.rs` only after the existing
  bootstrap phase completes.
- Change the run-family and run-adjacent entrypoints to accept context
  instead of raw `cli` / `cli_layer` / `printer` bundles.
- Replace direct reads of `cli.output.format` and
  `cli.output.verbosity` with `ctx.user_settings().cli.output.*`.
- Replace direct `process_local_json` threading with a context helper
  for the small number of commands that still need it (`graph`,
  `commands/sandbox/mod.rs` for the public preview/ssh boundary).
- Move the public `sandbox preview` / `sandbox ssh` dispatch boundary to
  the same context-first shape as the run-family boundary, while leaving
  the leaf implementation modules in `commands/run/*` if that remains
  the cleanest internal organization.
- Keep `Styles` local to the leaf commands and keep `attach` / `start`
  client helpers narrow if broadening them adds no value.

**Execution note:** Start by preserving or expanding characterization
coverage for JSON/text output and global `--json` gating before removing
the old raw-plumbing signatures from these entrypoints.

**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/run/create.rs`
- `lib/crates/fabro-cli/src/commands/preflight.rs`
- `lib/crates/fabro-cli/src/commands/graph.rs`
- `lib/crates/fabro-cli/src/commands/sandbox/mod.rs`

**Test scenarios:**
- Happy path: `fabro run --detach` still prints a bare run ID in text
  mode and the same JSON payload in JSON mode after output format moves
  behind `CommandContext`.
- Happy path: `fabro run` / `resume` / `attach` still inherit verbose
  rendering from CLI output verbosity and preserve idle-sleep behavior.
- Edge case: `fabro graph --json` without an explicit output file still
  rejects the global JSON override exactly as it does today.
- Edge case: `fabro sandbox preview --open` still opens the browser only
  when global JSON mode is not active.
- Edge case: `fabro sandbox ssh` still allows `--print` under global
  JSON mode but continues to reject the unsupported interactive
  combination.
- Error path: `preflight` and `validate` continue to fail on validation
  errors with the same text-vs-JSON contract and exit behavior.
- Integration: create -> start -> attach and resume/recovery flows still
  resolve targets, stream output, and summarize results through the same
  black-box CLI contracts.

**Verification:**
- `main.rs` no longer passes the raw plumbing bundle into the run /
  preflight / validate / graph / sandbox preview-ssh boundary.
- Those command boundaries obtain shared invocation data exclusively via
  `CommandContext`.

- [x] **Unit 3: Migrate runs/artifact/store families to context-first family dispatch**

**Goal:** Remove repeated target-context reconstruction from the command
families that already build a target-based `CommandContext` immediately
and mostly use raw CLI state only for output mode.

**Requirements:** R1, R3, R4, R6, R7

**Dependencies:** Units 1-2

**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/runs/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/archive.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/rm.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/inspect.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/cp.rs`
- Modify: `lib/crates/fabro-cli/src/commands/store/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/store/dump.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/archive.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/inspect.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/rm.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/store_dump.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/store.rs`

**Approach:**
- Have each family receive a context-first boundary and derive its
  target-based command context once, close to the namespace/selector
  boundary.
- Switch output-mode branches to
  `ctx.user_settings().cli.output.format`.
- Preserve existing client-sharing patterns such as
  `ServerSummaryLookup::from_client(ctx.server().await?)`.
- Keep leaf helpers narrow where they already only need a resolved
  client, resolved run ID, or printer.

**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- `lib/crates/fabro-cli/src/commands/artifact/mod.rs`

**Test scenarios:**
- Happy path: `runs list`, `archive`, `unarchive`, and `rm` still render
  the same JSON and text shapes after output-mode decisions move behind
  the context.
- Happy path: `runs inspect` remains a JSON-only projection of server
  state and still resolves the selected run before fetching the state.
- Edge case: “no runs found” and “no artifacts found” text-mode messages
  remain unchanged.
- Error path: ambiguous or missing run selectors still fail through the
  existing server/client resolution path.
- Integration: artifact listing/copy and store dump continue to hit the
  same server-backed data path and respect output-format selection.

**Verification:**
- These family dispatchers no longer need separate `cli_layer` and
  `printer` arguments merely to reconstruct a target context.

- [x] **Unit 4: Migrate PR, secret, and system command families**

**Goal:** Align the families that mix local settings, target-based
server access, and storage-dir-aware server access onto the same
context-first boundary.

**Requirements:** R1, R3, R4, R6, R7

**Dependencies:** Units 1-3

**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/pr/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/create.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/view.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/merge.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/close.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/set.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/rm.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/info.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/df.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/events.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/prune.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_create.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_view.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_merge.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_close.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_set.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_rm.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_info.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_df.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_events.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_prune.rs`

**Approach:**
- Keep `pr`’s split between base-settings work and target-based run
  lookup, but move both sides onto a common context-first boundary so
  raw `cli_layer` / `printer` threading disappears from the family API.
- Preserve `secret`’s existing pattern of resolving the server once at
  the family boundary and passing the client into subcommands.
- For `system`, derive connection-aware contexts from the base
  invocation context so storage-dir override behavior remains explicit
  and unchanged.

**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/pr/mod.rs`
- `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- `lib/crates/fabro-cli/src/commands/system/mod.rs`

**Test scenarios:**
- Happy path: PR list/view/create/merge/close continue to render the same
  JSON/text contracts and still resolve GitHub credentials from merged
  local settings.
- Happy path: secret list/set/rm continue to resolve one server client at
  the namespace boundary and honor JSON mode.
- Happy path: system info/df/events/prune continue to use
  storage-dir-aware connection mode where appropriate.
- Edge case: missing secret / no matching runs-to-prune / empty PR list
  still produce the same user-visible text-mode outcomes.
- Error path: invalid storage-dir or connection resolution still fails
  before the command attempts remote work.
- Integration: the `system` family continues to respect explicit
  `--storage-dir` overrides and local daemon resolution semantics.

**Verification:**
- These family boundaries no longer accept raw plumbing bundles when the
  only reason was to build a `CommandContext` or inspect output mode.

- [x] **Unit 5: Finish remaining base/target context users**

**Goal:** Complete the context-first migration for the remaining in-scope
command surfaces that already use `CommandContext` but still expose raw
plumbing at their entrypoints.

**Requirements:** R1, R2, R3, R5, R6, R7

**Dependencies:** Units 1-4

**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/model.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/init.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/deinit.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/login.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/logout.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/status.rs`
- Modify: `lib/crates/fabro-cli/src/commands/provider/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/provider/login.rs`
- Modify: `lib/crates/fabro-cli/src/commands/config/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/version.rs`
- Modify: `lib/crates/fabro-cli/src/commands/doctor.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model_test.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo_init.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo_deinit.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/auth.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/provider.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/provider_login.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/config.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/version.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/doctor.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`

**Approach:**
- Move base-context-only command families (`auth`, `provider`, parts of
  `repo`) onto `CommandContext` so JSON gating and printer usage come
  from the shared invocation object.
- Use `ctx.machine_settings()` / `ctx.user_settings()` consistently for
  local settings lookups rather than parallel raw-CLI arguments.
- Preserve the special-case JSON contract for `auth status`: explicit
  invocation-global `--json` remains the only JSON trigger, even if
  resolved CLI settings say `output.format = json`.
- Keep `repo deinit` and similar pure-local leaf helpers narrow if a
  derived `json_output` boolean or `Printer` extracted from the context
  keeps the internal helper clearer than passing the full context.

**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/auth/login.rs`
- `lib/crates/fabro-cli/src/commands/auth/status.rs`
- `lib/crates/fabro-cli/src/commands/config/mod.rs`
- `lib/crates/fabro-cli/src/commands/version.rs`

**Test scenarios:**
- Happy path: `auth status` and `provider login` preserve global `--json`
  restrictions and still resolve the intended server target from merged
  settings.
- Happy path: `config`, `version`, `model`, and `doctor` continue to
  honor text-vs-JSON output without a direct `CliNamespace` parameter.
- Edge case: persisted `cli.output.format = "json"` without explicit
  global `--json` still leaves `auth status` on its current text-mode
  path.
- Edge case: explicit global `--json` still forces `auth status` onto
  its JSON output path even though the implementation no longer receives
  a raw `process_local_json` parameter.
- Edge case: `repo init` non-JSON progress output and JSON result
  payloads stay unchanged.
- Edge case: `repo deinit` still stays local and does not accidentally
  require server access just because the family boundary now uses
  `CommandContext`.
- Error path: missing auth sessions, invalid server targets, and missing
  GitHub access continue to fail with the same user-facing contract.

**Verification:**
- Remaining in-scope command entrypoints no longer expose the raw
  plumbing bundle as part of their public internal API.

- [x] **Unit 6: Remove migration scaffolding and prove the boundary is clean**

**Goal:** Delete transitional surface area and confirm the in-scope
command tree is consistently aligned on `CommandContext`.

**Requirements:** R1, R2, R6, R7

**Dependencies:** Units 1-5

**Files:**
- Modify: `lib/crates/fabro-cli/src/command_context.rs`
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Modify: in-scope command modules touched by transitional wrappers
- Test: `lib/crates/fabro-cli/tests/it/cmd/top_level.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`

**Approach:**
- Remove temporary wrappers or compatibility constructors that were only
  present to get through the migration.
- Delete stale comments and “still being wired through” dead-code
  annotations once the boundary is real.
- Do a final sweep to ensure in-scope command boundaries are not still
  taking `&CliNamespace`, `&CliLayer`, `Printer`, and
  `process_local_json` together out of habit.

**Patterns to follow:**
- Keep cleanup limited to true scaffolding removal; do not reopen command
  semantics or formatting behavior in the cleanup pass.

**Test scenarios:**
- Test expectation: none -- cleanup-only unit. Behavioral coverage should
  already exist from Units 1-5.

**Verification:**
- The command-boundary API is visibly simpler and consistent across the
  in-scope families.
- No in-scope command still looks half-migrated.

## System-Wide Impact

- **Interaction graph:** `main.rs` bootstrap -> base `CommandContext` ->
  family dispatchers -> derived target/connection contexts ->
  `server_client.rs` / `user_config.rs`. This touches nearly every
  user-facing CLI family that already talks to settings or server
  resolution.
- **Error propagation:** context-construction failures remain early and
  synchronous at the command boundary; server access errors still flow
  through `ctx.server().await?` and should not move deeper into render
  helpers.
- **State lifecycle risks:** the biggest correctness risk is accidental
  reuse of the wrong derived context, especially storage-dir-aware
  contexts in the `system` family and target-based contexts in run/PR
  flows.
- **API surface parity:** although this is an internal refactor, it
  touches external CLI contracts indirectly through JSON/text output,
  verbosity, global `--json` restrictions, and server-target resolution.
- **Integration coverage:** black-box command tests and scenario tests
  are the main safety net. Unit tests should only cover context
  construction/derivation and not replace CLI integration coverage.
- **Unchanged invariants:** tracing and upgrade bootstrap ordering stays
  in `main.rs`; `exec` keeps its distinct direct-provider path; server
  connection semantics stay in `server_client.rs`; `Styles` remain
  command-local.

## Risks & Dependencies

| Risk | Mitigation |
|------|------------|
| Output-mode drift when replacing `cli.output.format` reads with `ctx.user_settings().cli.output.format` | Keep characterization coverage in existing `cmd/*` tests for both text and JSON paths before deleting the raw parameters |
| Global `--json` behavior changes while moving `process_local_json` into the context | Add targeted coverage in `json_global.rs`, `auth.rs`, `provider_login.rs`, `graph.rs`, and sandbox preview/ssh command tests |
| Storage-dir-aware system commands accidentally derive a target-mode context instead of a connection-mode context | Keep connection-specific derivation explicit and cover `system_info`, `system_df`, `system_events`, and `system_prune` with CLI integration tests |
| `CommandContext` grows into a second god object | Keep explicit scope rules: invocation plumbing only, no command args, no `Styles`, no feature-specific render state |
| The refactor becomes a giant compile-fix patch with poor reviewability | Land the work family-by-family with temporary compatibility wrappers where needed, and verify each family with its existing tests before cleanup |

## Documentation / Operational Notes

- No user-facing documentation changes are expected.
- Internal comments in `command_context.rs` and nearby command modules
  should be updated to describe the final boundary, not the transitional
  “still being wired through” state.
- The earlier April 8 plan should remain as historical context; this plan
  supersedes it for the command-boundary alignment work.

## Sources & References

- Prior plan: `docs/plans/2026-04-08-cli-services-command-context-refactor-plan.md`
- Related plan: `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
- Related code:
  `lib/crates/fabro-cli/src/command_context.rs`
  `lib/crates/fabro-cli/src/main.rs`
  `lib/crates/fabro-cli/src/server_client.rs`
  `lib/crates/fabro-cli/src/commands/run/mod.rs`
  `lib/crates/fabro-cli/src/commands/pr/mod.rs`
  `lib/crates/fabro-cli/src/commands/secret/mod.rs`
  `lib/crates/fabro-cli/src/commands/auth/mod.rs`
  `lib/crates/fabro-cli/src/commands/provider/mod.rs`
  `lib/crates/fabro-cli/src/commands/system/mod.rs`
- Testing guidance: `docs-internal/testing-strategy.md`
- Related history:
  `93b6577cd simplify: drop duplicate settings plumbing from cli/server refactor`
  `367fd9302 refactor(cli): centralize command settings and server access`
  `4b30a5f16 refactor(cli): route command output through Printer`
