Skip to content
156 changes: 156 additions & 0 deletions docs/ai/design/2026-08-09-feature-capacity-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
phase: design
title: Capacity Command Design
description: Architecture and security design for normalized provider capacity reporting
---

# Capacity Command Design

## Architecture Overview

```mermaid
flowchart LR
CLI[capacity command] --> Detect[Configured-provider detection]
Detect --> Orchestrator[Parallel orchestrator]
Orchestrator --> Cache[(Normalized cache)]
Orchestrator --> Codex[Codex adapter]
Orchestrator --> Claude[Claude adapter]
Orchestrator --> Pi[Pi / GLM adapter]
Orchestrator --> Stub[Unsupported-provider stub]
Codex --> AppServer[codex app-server]
Claude --> AuthStatus[claude auth status]
Pi --> PiAuth[Pi auth provider names]
Orchestrator --> Report[CapacityReport v1]
Report --> Human[Human table]
Report --> JSON[JSON output]
```

The Commander registration layer delegates to a report orchestrator. Detection, provider adapters, normalization, cache, and rendering are separate modules with dependency injection at subprocess and orchestration boundaries.

## Command API

```text
capacity [provider] [--json] [--max-age <seconds>] [--refresh]
```

- No provider: detect only configured providers.
- Provider: request one known provider even if it is not configured, while reporting its actual state.
- `--json`: serialize the report with two-space indentation.
- `--max-age`: accept a non-negative integer; default 300 seconds.
- `--refresh`: skip cache lookup.

Invalid arguments fail before probing. A constructed report exits successfully even if some rows are unknown.

## State Model

These signals are independent:

| Signal | Meaning | Source |
|---|---|---|
| `configured` | Provider configuration directory exists | `ENVIRONMENT_DEFINITIONS.globalSkillPath` |
| `installed` | Expected executable exists and is executable on PATH | executable access check |
| `authenticated` | Provider-specific probe found valid authentication | app-server/auth status/Pi provider keys |

Provider status is one of `supported`, `unsupported`, `unauthenticated`, `unavailable`, or `unknown`. Availability is separately `yes`, `no`, or `unknown`.

## Data Model

```ts
type CapacityWindow = {
id: string;
label: string;
durationMinutes: number | null;
usedPercent: number | null;
remainingPercent: number | null;
resetsAt: string | null;
scope: string | null;
};

type ProviderCapacity = {
provider: string;
agentType: string | null;
configured: boolean;
installed: boolean;
authenticated: boolean | null;
status: 'supported' | 'unsupported' | 'unauthenticated' | 'unavailable' | 'unknown';
available: 'yes' | 'no' | 'unknown';
plan: string | null;
checkedAt: string;
source: 'provider-cli' | 'provider-api' | 'local-observation' | 'none';
windows: CapacityWindow[];
aliases: { dailyWindowId: string | null; weeklyWindowId: string | null };
resetCredits?: { available: number | null };
warnings: Array<{ code: string; message: string }>;
error?: { code: string; retryable: boolean };
};

type CapacityReport = {
schemaVersion: 1;
generatedAt: string;
providers: ProviderCapacity[];
};
```

`windows` is canonical. Aliases are derived by duration tolerance around 1,440 and 10,080 minutes. Native scoped windows remain separate, duplicate compatibility buckets are removed by normalized ID, and `remainingPercent` is derived only from an authoritative numeric `usedPercent`.

## Configured-Provider Detection

`detection.ts` reuses `ENVIRONMENT_DEFINITIONS`; it does not maintain a second provider-to-config mapping. The root is derived from `globalSkillPath` (including nested `.config/<provider>` roots), joined to the user home directory, and checked for existence. GitHub environment naming is normalized to provider name `copilot`. Binary detection is a separate executable-access scan over PATH and never establishes configuration.

## Provider Adapters

### Codex

```mermaid
sequenceDiagram
participant C as capacity
participant A as codex app-server --stdio
C->>A: initialize(clientInfo, capabilities=null)
A-->>C: initialize result
C->>A: initialized
C->>A: account/rateLimits/read
A-->>C: rateLimits + buckets + reset-credit summary
C->>C: sanitize, normalize, deduplicate, derive aliases
```

The JSON-line transport is injectable in tests. It ignores stderr, bounds execution with a timeout, kills the child after completion, and exposes only normalized fields. It never invokes `turn/start`, `codex exec`, or another model method. The mapper supports the current `rateLimitResetCredits` field plus the older compatibility name, reports `availableCount`, and has no consume/redeem operation.

### Claude

The adapter runs `claude auth status --json` with bounded stdout and a timeout. Claude may return valid logged-out JSON with a nonzero exit, so that bounded stdout is parsed while stderr and exception text are discarded. The undocumented OAuth usage endpoint is not called; capacity remains unknown even when authentication succeeds.

### Pi and GLM

The adapter reads `~/.pi/agent/auth.json`, retains only top-level provider names, and never emits credential values. Any configured Pi credential establishes Pi authentication. `zai` or `zai-coding-cn` additionally establishes GLM authentication. Both remain unsupported/unknown because no verified account-quota reader exists.

### Other Providers

Configured providers without an authoritative adapter use the common stub. The stub preserves configured/installed state, maps to the correct AI DevKit `agentType` when available, and returns `status: unsupported`, `available: unknown`.

## Orchestration and Cache

- Provider probes execute with `Promise.all` and a seven-second orchestration timeout; adapters also apply their own subprocess timeouts.
- Exceptions become fixed-code unknown rows. Raw exception data is discarded.
- Cache keys distinguish explicit-provider and configured-provider sets.
- The default cache path is `~/.ai-devkit/cache/capacity.json`.
- Cache directory mode is `0700`; file and temporary file mode is `0600`; writes use rename.
- Cache failures never prevent a report, and `--refresh` bypasses reads.

## Security and Reliability Decisions

- Provider CLIs own OAuth/session authentication; secrets are not passed on command lines.
- Output and cache contain normalized allowlisted data, not raw responses.
- Codex identifiers, labels, and plan metadata are validated and credential/account-like values are rejected.
- Claude plan metadata is similarly constrained.
- Error output uses fixed codes/messages; stderr, URLs, headers, bodies, and exception text are never rendered.
- Unknown data remains unknown. Stubs and probe failures cannot claim availability.
- Partial failure is isolated so one provider cannot suppress other results.

## Alternatives Rejected

- Direct private HTTP calls: excessive credential exposure and undocumented coupling.
- TUI scraping: brittle and capable of accidentally starting model activity.
- Local token-history estimation: not authoritative for subscription limits.
- Forced daily/weekly schema: loses provider-native rolling and scoped windows.

The original structured capacity brainstorm supplied the deeper provider feasibility analysis; this document records the architecture that actually shipped.
100 changes: 100 additions & 0 deletions docs/ai/implementation/2026-08-09-feature-capacity-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
phase: implementation
title: Capacity Command Implementation Record
description: Shipped modules, integration points, invariants, and operational behavior
---

# Capacity Command Implementation Record

## Shipped Module Map

```text
packages/cli/src/
├── cli.ts
└── commands/
├── capacity.ts
└── capacity/
├── types.ts
├── detection.ts
├── orchestrate.ts
├── cache.ts
├── render.ts
└── providers/
├── codex.ts
├── claude.ts
├── pi.ts
└── stub.ts
```

Tests live in `packages/cli/src/__tests__/commands/capacity/`.

## CLI Registration

`cli.ts` imports and calls `registerCapacityCommand(program)`. `commands/capacity.ts` owns Commander configuration, validates `--max-age`, calls `getCapacityReport`, and hands the result to `renderCapacityReport`. It exposes only:

```text
capacity [provider] [--json] [--max-age <seconds>] [--refresh]
```

## Module Responsibilities

- `types.ts`: exact schema-v1 TypeScript contract.
- `detection.ts`: derives provider config directories from `ENVIRONMENT_DEFINITIONS.globalSkillPath` and independently checks executable access on PATH.
- `orchestrate.ts`: validates provider names, selects configured providers by default, runs probes concurrently, isolates failures/timeouts, reads/writes cache, sorts rows, and constructs the report.
- `cache.ts`: reads freshness-keyed normalized reports and performs atomic restrictive writes under `~/.ai-devkit/cache/capacity.json` (`0700` directory, `0600` file).
- `render.ts`: emits exact pretty JSON or a text table with Auth, Available, shortest/longest native windows, reset credits, and warnings.
- `providers/codex.ts`: drives app-server JSON-RPC and sanitizes/normalizes rate-limit snapshots.
- `providers/claude.ts`: invokes and safely parses `claude auth status --json`; does not fetch live quota.
- `providers/pi.ts`: reads only Pi auth provider names and derives Pi/GLM authentication.
- `providers/stub.ts`: builds truthful unsupported/unknown rows for providers without adapters.

## Codex JSON-RPC Client

The adapter spawns `codex app-server --stdio` with piped stdin/stdout and ignored stderr. It writes newline-delimited JSON:

1. `initialize` with `clientInfo` and `capabilities: null`.
2. After response id 1, `initialized`.
3. `account/rateLimits/read` with request id 2 and no parameters.

Response id 2 is normalized and the subprocess is terminated. A five-second adapter timer bounds the exchange. The transport function is injectable, so CI tests use no subprocess or network.

Mapping behavior:

- Normalize backward-compatible `rateLimits` and `rateLimitsByLimitId` snapshots.
- Preserve primary/secondary windows by scoped ID and remove duplicates.
- Convert epoch reset timestamps to ISO-8601.
- Clamp derived remaining percent to 0–100.
- Derive daily/weekly aliases by duration tolerance only.
- Treat a reported reached type as explicit `available: no`; missing windows remain unknown.
- Report only reset-credit `availableCount`; no consume method exists.

## Provider Detection and Unknown Semantics

The default row set is determined before binary checks. Configured, installed, and authenticated are stored independently. A configured but uninstalled provider remains visible. An installed but unconfigured provider does not enter the default report. Explicitly requested known providers are reported even when unconfigured.

Only authoritative Codex utilization can establish `available: yes`. Claude, Pi, GLM, and unsupported providers remain `available: unknown` without verified quota data.

## Failure Handling

- Adapter exceptions never escape into report text.
- Orchestration catches each provider independently and emits a retryable fixed-code unknown row.
- Cache read/write failures are non-fatal.
- Unknown providers and invalid max-age values are command errors.
- Claude logged-out JSON is accepted from bounded stdout even when the CLI returns nonzero; stderr remains unused.
- A report, including a partial report, exits successfully.

## Security Invariants

- No tokens, account IDs, refresh tokens, endpoint URLs, headers, raw bodies, stderr, or raw exception messages are emitted or cached.
- No credential is placed on a subprocess command line.
- Codex authentication and refresh remain inside Codex app-server.
- Codex labels, IDs, scopes, and plans are constrained before output; Claude plan metadata is constrained too.
- Pi credential values are parsed only to discover top-level provider names and are never retained in normalized output.
- Cache contains only normalized report data with restrictive permissions.
- Capacity checks contain no model-start/inference method and never redeem reset credits.

## Design Alignment and Deviations

The shipped implementation matches the locked design. The brainstorm considered guarded use of Claude's undocumented OAuth usage endpoint; implementation review rejected that risk and shipped authentication-only Claude support. The brainstorm's broader draft schema contained fields such as transport provider and stale-after metadata; schema v1 intentionally uses the smaller contract in `types.ts`.

No code change, data migration, new dependency, or rollout flag is required for these lifecycle documents.
69 changes: 69 additions & 0 deletions docs/ai/planning/2026-08-09-feature-capacity-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
phase: planning
title: Capacity Command Implementation Plan
description: Completed task record for the shipped capacity command
---

# Capacity Command Implementation Plan

All tasks are complete. The list reflects execution order and the pushed commit that delivered each outcome.

## Milestone 1: Detection and Core Contract

- [x] Define schema-v1 capacity types and configuration/PATH detection — `c6c386b`.
- Outcome: `CapacityReport`, `ProviderCapacity`, arbitrary `CapacityWindow[]`, and independent configured/installed checks.
- Validation: detection derives config roots from `ENVIRONMENT_DEFINITIONS` and never runs provider binaries.
- [x] Build the Codex app-server adapter under TDD — `d57813a`.
- Outcome: injectable JSON-line transport, normalized windows, aliases, availability, plan, and reset-credit count.
- Validation: mocked protocol sequence contains no model-turn method and failures are redacted.

## Milestone 2: Provider Coverage and Orchestration

- [x] Add truthful Claude, Pi, GLM, and unsupported-provider adapters — `c614f27`.
- Outcome: Claude auth detection, Pi provider-name inspection, GLM detection through z.ai keys, and unknown-capacity stubs.
- Validation: injected secrets and thrown response details do not reach reports.
- [x] Add parallel orchestration and secure cache — `5de3a72`.
- Outcome: configured-only default, explicit provider validation, partial-result isolation, timeouts, max-age/refresh behavior, atomic restrictive cache.
- Validation: mocked adapters prove parallel selection, cache reuse/bypass, and partial failure behavior.

## Milestone 3: CLI and Presentation

- [x] Register and document the command — `69a201d`.
- Outcome: `registerCapacityCommand` in `cli.ts`, locked options, JSON rendering, human table, warnings, and CLI README examples.
- Validation: Commander integration forwards the provider and parsed cache options; invalid max-age fails before probing.

## Milestone 4: Live-Protocol and Security Hardening

- [x] Align with the generated Codex app-server protocol — `c04ea1f`.
- Outcome: exact initialize payload, parameterless rate-limit read, current reset-credit field, duplicate bucket removal, and identifier redaction.
- Validation: generated-protocol assertions and a live read-only Codex smoke test.
- [x] Harden provider metadata and agent-type mappings — `f34dbc3`.
- Outcome: reject credential/account-like plan metadata; map Gemini, Grok, and Copilot to shipped agent types.
- Validation: redaction and mapping regression tests.
- [x] Correct logged-out Claude handling — `5e2cc89`.
- Outcome: accept bounded JSON stdout from Claude's expected nonzero logged-out exit and use a six-second adapter timeout under the seven-second orchestrator guard.
- Validation: mocked nonzero behavior plus live `authenticated: false` classification.

## Dependencies and Sequencing

1. Types and detection established the provider/report contract.
2. Provider adapters normalized into that contract.
3. Orchestration composed adapters and added cache/timeout behavior.
4. CLI/rendering exposed the report.
5. Full tests and real read-only probes drove protocol/security fixes.

Runtime dependencies are Node.js, Commander, provider CLIs already installed by the user, and the existing AI DevKit environment definitions. No new package dependency or migration was introduced.

## Risks and Mitigations

- Codex app-server protocol changes: capability failures degrade to unknown; transport and mapping are isolated and tested.
- Undocumented Claude usage endpoint: not used; authentication-only output is explicit.
- Provider failure/latency: parallel probes, subprocess/orchestrator timeouts, and partial results.
- Secret leakage: provider-owned auth, bounded streams, fixed errors, field sanitization, and restrictive normalized cache.
- Misleading capacity: positive availability requires authoritative data; unsupported/missing data remains unknown.

## Deferred Follow-Ups

- Add Claude live capacity only if a safe provider-owned command becomes available.
- Add GLM or other provider adapters only after verifying authoritative, non-inference quota mechanisms.
- Add scheduling/recommendation policy separately from factual collection.
Loading
Loading