# Claude Code Account Parity Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Give Claude Code accounts the same tray account management, authentication health, and usage-limit functionality as Codex accounts.

**Architecture:** Replace Codex-specific UI dispatch with tool-qualified provider services. Add transactional Claude login, concurrency-safe OAuth refresh/usage retrieval, and a Claude health provider whose authentication result remains independent from the internal usage endpoint.

**Tech Stack:** Python 3.13, GTK 3 / AyatanaAppIndicator, `subprocess`, `urllib.request`, `fcntl`, pytest, Ruff, mypy.

---

**Design source:** `docs/superpowers/specs/2026-07-03-claude-account-parity-design.md`

**Executor:**

```bash
node /home/user/.claude/workflows/run-plan-codex.js \
  --slug claude-account-parity \
  --repo /home/user/Projects/systray-ai \
  --concurrency 3
```

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|---|---|---|---|
| 1 | 1.1 Provider contracts; 1.2 Registry transactions; 1.3 OAuth session | Separate source and test files | Yes; no semantic or file overlap |
| 2 | 2.1 Claude lifecycle; 2.2 Claude health/limits | Separate source and test files | Yes; both consume Wave 1 only |
| 3 | 3.1 Provider-aware UI; 3.2 Redacted smoke probe | UI files versus new script files | Yes; no file overlap |
| 4 | 4.1 Startup/scheduler/router integration | Composition-root and integration tests | Single task |

`meta.scheduler` is `dag-parallel`: Waves 1, 2, and 3 contain independent file-disjoint tasks. `run-plan-codex.js` enforces wave ordering and accepts `--concurrency 3`.

## File Map

| File | Responsibility |
|---|---|
| `provider_services.py` | Provider-neutral lifecycle/health contracts, service record/map, Codex lifecycle adapter |
| `account_registry.py` | Staged account add, rollback-safe removal, Claude identity sidecar lookup |
| `claude_oauth.py` | OAuth usage request, one refresh retry, credential locking/atomic merge, redaction |
| `claude_auth_operation.py` | Claude browser-login add and re-authentication transactions |
| `claude_health_client.py` | Claude auth status plus five-hour/seven-day usage mapping |
| `health_client.py`, `health_store.py` | Provider-neutral snapshot extensions and persistence |
| `indicator.py`, `account_card.py` | Provider-qualified account actions and health/limit presentation |
| `scripts/verify_claude_health.py` | Opt-in, explicit-account, redacted live schema probe |
| `systray_codex_switcher.py`, `scheduler.py` | Composition root and dual-provider refresh wiring |

## Task 1.1: Provider Contracts And Codex Adapter

**Wave:** 1
**Blocks:** Tasks 2.1, 2.2, 3.1, 4.1
**Blocked by:** -

**Files:**
- Create: `provider_services.py` - provider contracts, event type, service map, Codex adapter
- Create: `tests/test_provider_services.py` - contract and dispatch tests

**Contract (pin exactly):**
- `LifecycleEvent`: frozen dataclass carrying `kind`, `account`, `message`, `error`, `prompt`, `session`, `collision`, and `existing_accounts`; optional fields default to `None`/empty tuple.
- `AccountLifecycle.add(alias: str, *, login_hint: str | None = None) -> Iterator[LifecycleEvent]`.
- `AccountLifecycle.reauthenticate(account: Account) -> Iterator[LifecycleEvent]`.
- `AccountHealthProvider.fetch(account: Account, timeout_secs: float = 10.0) -> AccountSnapshot`.
- `ProviderServices(tool: AccountRegistryKind, registry: AccountRegistry, lifecycle: AccountLifecycle, health: AccountHealthProvider)` is frozen.
- `ProviderServiceMap.for_tool(tool: AccountRegistryKind | str) -> ProviderServices` and `.for_account(account: Account) -> ProviderServices`; normalize strings through `AccountRegistryKind`, reject duplicate/missing providers.
- `CodexLifecycleAdapter` translates every existing `DeviceAuthOperation` event field without changing Codex behavior; `login_hint` is ignored for Codex.

**Behavior:**
- Keep provider selection tool-qualified; same slugs across Codex/Claude must not collide.
- Do not import GTK or perform I/O in this module.
- Preserve existing Codex add/repair rollback, collision, prompt, and session events.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_provider_services.py tests/test_device_auth_operation.py -q`
- Expected: PASS; Codex adapter event parity and duplicate/missing provider rejection covered.

- [ ] Write focused failing tests for the contract and Codex translation.
- [ ] Implement the provider contracts and adapter.
- [ ] Run the acceptance check.
- [ ] Commit: `git add provider_services.py tests/test_provider_services.py && git commit -m "feat: add provider service contracts"`

## Task 1.2: Transactional Registry Operations

**Wave:** 1
**Blocks:** Tasks 2.1, 3.1
**Blocked by:** -

**Files:**
- Modify: `account_registry.py:128-420` - staged add, identity sidecar, transactional remove
- Modify: `tests/test_account_registry.py:349-457` - transaction and migration coverage

**Contract (pin exactly):**
- `AccountRegistry.stage_add(alias: str) -> StagedAccount` creates an unregistered `.staging-*` root and provider account home.
- `StagedAccount.account_home: Path`, `.slug: str`, `.commit() -> Account`, `.rollback() -> None`; commit/rollback are single-use and a second terminal call raises `RuntimeError("staged account already closed")`.
- Commit acquires an account-root lock, rejects an existing final slug/directory, atomically renames staging, then atomically writes registry state; failure restores/removes all staged state.
- Claude `list()` reads `account_identity.json` before legacy `claude.json`; sidecar schema is `{"email": str, "org_id": str, "subscription_type": str | null}` and maps `org_id` to `Account.account_id`, `subscription_type` to `Account.plan`.
- `remove(slug: str) -> None` quarantines the account directory, atomically updates registry/default/active links, deletes quarantine after commit, and restores quarantine on state-write failure.
- Removing a default selects the first remaining registry entry; removing the final account clears default and active Claude/Codex credential links.

**Behavior:**
- `list()` never exposes staging/quarantine directories.
- Concurrent destination creation fails closed without overwrite.
- Preserve existing `add_dir()` and Codex migration behavior.
- All tests use temporary homes; never touch live accounts.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_account_registry.py tests/test_command_router.py -q`
- Expected: PASS; add visibility, rollback, collision, default reassignment, final removal, and write-failure restoration covered.

- [ ] Write failing transaction and identity-sidecar tests.
- [ ] Implement staged add and rollback-safe remove under a narrow lock.
- [ ] Run the acceptance check.
- [ ] Commit: `git add account_registry.py tests/test_account_registry.py && git commit -m "feat: make account changes transactional"`

## Task 1.3: Claude OAuth Usage Session

**Wave:** 1
**Blocks:** Task 2.2
**Blocked by:** -

**Files:**
- Create: `claude_oauth.py` - OAuth transport, refresh, locking, atomic credential merge
- Create: `tests/test_claude_oauth.py` - fake-transport security and race tests

**Contract (pin exactly):**
- Constants: `USAGE_URL = "https://api.anthropic.com/api/oauth/usage"`, `TOKEN_URL = "https://platform.claude.com/v1/oauth/token"`, `CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"`.
- `ClaudeOAuthSession(credentials_path: Path, *, transport: JsonTransport | None = None, timeout_secs: float = 5.0)`.
- `get_usage() -> dict[str, object]` performs one authenticated GET and at most one refresh/retry after 401.
- Refresh request carries `grant_type=refresh_token`, current `refresh_token`, and the pinned client ID.
- Lock adjacent to credentials; after lock acquisition re-read credentials and use a token another process already replaced instead of refreshing stale state.
- Atomic merge preserves file mode, `mcpOAuth`, and unknown top-level/OAuth keys.
- Typed failures distinguish credentials, authentication, protocol/schema, timeout, and transport errors.

**Behavior:**
- Do not retry 403/404/schema failures.
- Never print/log token values, Authorization headers, identity-bearing response bodies, or request objects.
- Exception messages must redact access and refresh token values.
- Tests inject transport and credentials; no live HTTP.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_claude_oauth.py -q`
- Expected: PASS; 401 refresh, concurrent replacement, unknown-key preservation, atomic mode preservation, retry bounds, and redaction covered.

- [ ] Write failing fake-transport tests.
- [ ] Implement the OAuth session and typed failures.
- [ ] Run the acceptance check.
- [ ] Commit: `git add claude_oauth.py tests/test_claude_oauth.py && git commit -m "feat: add Claude OAuth usage session"`

## Task 2.1: Claude Account Lifecycle

**Wave:** 2
**Blocks:** Tasks 3.1, 4.1
**Blocked by:** Tasks 1.1, 1.2

**Files:**
- Create: `claude_auth_operation.py` - Claude add/repair state machine
- Create: `tests/test_claude_auth_operation.py` - subprocess and rollback tests

**Contract (pin exactly):**
- `ClaudeAuthOperation(registry: AccountRegistry, *, runner: ClaudeAuthRunner | None = None)` implements `AccountLifecycle`.
- Add command: `claude auth login --claudeai`; append `--email <login_hint>` only when non-empty; set `CLAUDE_CONFIG_DIR` to staged `CLAUDE_HOME`.
- Verify exit code zero, `.credentials.json`, and bounded `claude auth status --json` with `loggedIn: true`.
- Normalize identity as lowercase trimmed email plus `orgId`; reject an identity owned by another registry account.
- Persist `account_identity.json` atomically with email, `org_id`, and `subscriptionType`.
- Re-authentication locks account home, backs up credentials/identity/Claude metadata, requires the known identity to remain unchanged, and restores all backups on cancel/failure/mismatch/collision.
- Legacy repair with no recoverable prior identity accepts a unique verified identity as baseline.

**Behavior:**
- Emit provider-neutral events: started, browser-waiting, success, collision, failure, rollback.
- Cancellation reports cancellation and restores/removes state; it does not mark account broken.
- Never invoke `claude auth logout` during local removal.
- Do not expose subprocess output containing identity or credentials in error text.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_claude_auth_operation.py tests/test_account_registry.py -q`
- Expected: PASS; add/repair success, cancel, malformed status, duplicate/wrong identity, legacy baseline, and restoration covered.

- [ ] Write failing lifecycle tests with fake runner/status responses.
- [ ] Implement Claude add and re-authentication transactions.
- [ ] Run the acceptance check.
- [ ] Commit: `git add claude_auth_operation.py tests/test_claude_auth_operation.py && git commit -m "feat: add Claude account lifecycle"`

## Task 2.2: Claude Health And Limits

**Wave:** 2
**Blocks:** Tasks 3.1, 3.2, 4.1
**Blocked by:** Tasks 1.1, 1.3

**Files:**
- Create: `claude_health_client.py` - auth status and usage mapping
- Create: `tests/test_claude_health_client.py` - auth/usage independence tests
- Modify: `health_client.py:14-32` - provider-neutral snapshot/named limit fields
- Modify: `health_store.py:48-102` - snapshot persistence compatibility
- Modify: `tests/test_health_client.py` - snapshot compatibility
- Modify: `tests/test_health_store.py` - new-field round trips and old-cache reads

**Contract (pin exactly):**
- `ClaudeHealthClient.fetch(account: Account, timeout_secs: float = 10.0) -> AccountSnapshot`.
- Run `claude auth status --json` with target `CLAUDE_CONFIG_DIR`; `loggedIn: true` maps to `HealthStatus.OK`, explicit false/auth rejection to `BROKEN`, launch/timeout/malformed/transient failure to `UNKNOWN`.
- `NamedLimit(kind: str, group: str | None, percent: int, resets_at: float | None, active: bool)` is frozen.
- Extend `AccountSnapshot` backward-compatibly with `checked_at`, redacted `detail`, `named_limits`, and typed extra-usage/spend summary fields with defaults.
- Map `five_hour.utilization/resets_at` to primary percent/reset and `seven_day` to secondary; clamp percentages to `0..100`; parse ISO timestamps to epoch seconds.
- Preserve active `limits` entries and cache extra-usage/spend summaries; ignore unknown response keys and accept null optional fields.
- Health store reads old cache entries without new fields and round-trips new fields.

**Behavior:**
- Usage 401-after-refresh, 403, 404, timeout, or schema change retains verified `OK` auth with unavailable limit fields and redacted detail.
- Auth and usage have independent failure domains.
- Do not consume model quota or scrape `/usage` terminal output.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_claude_health_client.py tests/test_health_client.py tests/test_health_store.py tests/test_routing_resolver.py -q`
- Expected: PASS; health mapping, limits/reset parsing, null/unknown fields, backward cache reads, and usage-failure independence covered.

- [ ] Write failing health, parser, and store compatibility tests.
- [ ] Implement snapshot extensions and Claude health provider.
- [ ] Run the acceptance check.
- [ ] Commit: `git add claude_health_client.py health_client.py health_store.py tests/test_claude_health_client.py tests/test_health_client.py tests/test_health_store.py && git commit -m "feat: add Claude health and limits"`

## Task 3.1: Provider-Aware Tray And Popup

**Wave:** 3
**Blocks:** Task 4.1
**Blocked by:** Tasks 1.1, 1.2, 2.1, 2.2

**Files:**
- Modify: `indicator.py:99-967` - provider service dispatch and dual refresh
- Modify: `account_card.py:75-273` - lifecycle controls and health presentation
- Modify: `tests/test_indicator.py:668-2417` - dual-provider lifecycle/refresh tests
- Modify: `tests/test_account_card.py` - action and stale-limit presentation tests

**Contract (pin exactly):**
- `Indicator` receives `ProviderServiceMap`; every action resolves services using `account.tool`/`account.tray_key`.
- Render provider-scoped Add Account and per-account Set Default, Reload, Re-authenticate, Rename, Remove actions for Codex and Claude.
- Key cards, buttons, snapshots, and in-flight state by `Account.tray_key` (`"<tool>:<slug>"`).
- Account card callbacks carry the `Account` object, never a bare slug.
- Refresh All and popup-open schedule every provider account; persistence remains in separate Codex/Claude health stores.
- Merge usage-only failures with prior successful limits/reset timestamps while showing current auth state and stale timestamp.

**Behavior:**
- Disable only the action/account currently running.
- Rebuild tool-qualified menu/card state after add, rename, remove, default change, and re-authentication.
- Preserve all existing Codex copy, actions, lazy refresh, and auto-close behavior.
- Same-slug accounts across providers remain independent.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_indicator.py tests/test_account_card.py tests/test_popup_window.py tests/test_tray_model.py -q`
- Expected: PASS; both providers expose/dispatch lifecycle actions and refresh without slug collisions or Codex regressions.

- [ ] Write failing dual-provider UI and stale-limit tests.
- [ ] Refactor UI dispatch and render Claude parity actions.
- [ ] Run the acceptance check.
- [ ] Commit: `git add indicator.py account_card.py tests/test_indicator.py tests/test_account_card.py && git commit -m "feat: add Claude account actions to tray"`

## Task 3.2: Redacted Claude Health Smoke Probe

**Wave:** 3
**Blocks:** -
**Blocked by:** Task 2.2

**Files:**
- Create: `scripts/verify_claude_health.py` - opt-in explicit-home probe
- Create: `tests/test_verify_claude_health.py` - output/redaction tests

**Contract (pin exactly):**
- CLI requires `--account-home PATH`; never default to live `~/.claude`.
- Reuse `ClaudeHealthClient` and print one JSON object containing only `status`, `five_hour_available`, `seven_day_available`, `five_hour_reset_valid`, `seven_day_reset_valid`, and `named_limit_count`.
- Exit 0 only for `HealthStatus.OK` with both primary/secondary limits and reset timestamps; exit 1 otherwise; exit 2 for usage/argument errors.

**Behavior:**
- Output must never contain email, organization/account IDs, access/refresh tokens, paths, raw endpoint payloads, or Authorization headers.
- Tests use fake providers; automated gates never contact Anthropic.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_verify_claude_health.py -q`
- Expected: PASS; output schema, exit codes, explicit-home requirement, and secret/identity redaction covered.

- [ ] Write failing CLI output and redaction tests.
- [ ] Implement the opt-in probe.
- [ ] Run the acceptance check.
- [ ] Commit: `git add scripts/verify_claude_health.py tests/test_verify_claude_health.py && git commit -m "test: add Claude health smoke probe"`

## Task 4.1: Composition-Root And Scheduler Integration

**Wave:** 4
**Blocks:** -
**Blocked by:** Tasks 1.1, 2.1, 2.2, 3.1

**Files:**
- Modify: `systray_codex_switcher.py:117-144` - construct and inject both providers
- Modify: `scheduler.py:39-126` - provider-qualified refresh scheduling if required
- Modify: `tests/test_systray_codex_switcher.py:101-257` - composition-root tests
- Modify: `tests/test_scheduler.py` - dual-provider scheduling tests
- Modify: `tests/test_command_router.py` - fresh Claude cache routing integration

**Contract (pin exactly):**
- Startup constructs Codex `ProviderServices` with existing registry/health/device-auth adapter and Claude `ProviderServices` with Claude registry/auth/health.
- Pass one complete `ProviderServiceMap` into `Indicator`; provider-specific constructors remain outside UI modules.
- Run both registry migrations before service map creation.
- Scheduler accepts tool-qualified accounts and never indexes work by bare slug.
- Claude health writes `claude_health_cache.json`; `cld` consumes it independently from Codex `health_cache.json`.

**Behavior:**
- Keep refresh work off the GTK thread.
- Startup failure for one unsupported Claude configuration reports the existing explicit error without corrupting Codex state.
- Preserve single-instance locking, routing-rule seeding, and current command-router fallback behavior.

**Acceptance (one executable check):**
- Run: `python3 -m pytest tests/test_systray_codex_switcher.py tests/test_scheduler.py tests/test_command_router.py tests/test_indicator.py -q`
- Expected: PASS; both provider services are wired, scheduled, cached, and routed independently.

- [ ] Write failing composition, scheduler, and router integration tests.
- [ ] Wire both provider services at startup and qualify scheduler state.
- [ ] Run the acceptance check.
- [ ] Commit: `git add systray_codex_switcher.py scheduler.py tests/test_systray_codex_switcher.py tests/test_scheduler.py tests/test_command_router.py && git commit -m "feat: wire Claude account parity"`

## Automated Gate

`run-plan-codex.js` runs this after every task and before accepting its commit:

```bash
python3 -m pytest tests/ -q && ruff check . && mypy .
```

Expected: all commands exit 0. The executor performs Codex implementation, gate-fix retries, Codex review/fix retries, per-task commits, wave integration, and final ship according to its own workflow.

## Post-Run Manual Verification

Run after the executor completes; this is not a JSONL task because `run-plan-codex.js` requires every task to produce a commit.

1. Execute `python3 scripts/verify_claude_health.py --account-home ~/.systray-ai/claude-accounts/<verified-slug>/CLAUDE_HOME`; expect redacted healthy JSON and exit 0.
2. In GTK: add Claude account, set default, rename, reload, re-authenticate same identity, cancel a second re-authentication and confirm rollback, remove a non-default, remove a default and confirm reassignment.
3. Confirm Codex actions remain functional after each Claude action.

## Decision Enumeration

- No task requires a human decision. Architecture and internal endpoint use are fixed by the approved design and verified local Claude Code behavior.
- No plan task performs an irreversible live operation. Destructive account behavior is implemented and tested only against temporary registries.
- `base_branch=main` and `land_mode=pr` follow the repository’s existing `run-plan-codex` records.

## Self-Review

- Spec coverage: lifecycle, rename/remove/default, auth health, five-hour/seven-day limits, stale cache, dual-provider UI, scheduler/router, security, and smoke verification all map to tasks.
- Contract scan: no implementation bodies, placeholders, unnamed errors, or cross-task shorthand.
- Seam consistency: `ProviderServiceMap`, `AccountLifecycle`, `AccountHealthProvider`, `LifecycleEvent`, `StagedAccount`, `ClaudeOAuthSession`, `ClaudeAuthOperation`, `ClaudeHealthClient`, `NamedLimit`, and `AccountSnapshot` signatures align across tasks.
- Wave safety: same-wave tasks have disjoint files and no same-wave dependency.
- Scheduler: `dag-parallel` is required because three waves contain real parallel rounds.
