Skip to content

Investigate cross-agentic-CLI support (Codex, pi, Antigravity) #5

Description

@SeanLF

Summary

Feasibility investigation: how hard would it be to make ccpool work across other agentic CLIs (OpenAI Codex CLI, pi, Google Antigravity agy), not just Claude Code?

Short version: the governance loop is more portable than expected. Codex, pi, and Antigravity all have mid-turn hook systems with context injection, so the warn mechanic ports to all three. ccusage tracks API-equivalent $ for all of them, so the dollar layer ports too. What does not port is essentially one Claude-specific lever (the live subagent-downshift env var), plus a per-host usage-% adapter over data that is undocumented / header-only / RPC-only depending on the host (and absent entirely for pi).

This is a scoping issue to capture findings, not a commitment to build. Sources cited inline.

The four ccpool pillars vs each host (documented)

Pillar Claude Code (today) Codex CLI pi Antigravity agy
1. Read account usage % statusline payload on stdin ✅ verified rate_limits.primary in session JSONL (used_percent/window_minutes/resets_at; shape below); /status is the only interactive surface ✗ no normalized %; only raw provider headers via after_provider_response ⚠️ backend RPC FetchQuotaStatus, no local file
2. Mid-turn warn (hook → context) UserPromptSubmit/PostToolUse → additionalContext ✅ near-identical: same event names + hookSpecificOutput.additionalContext ✅ before_agent_start / context / tool_call inject into context ✅ plugins/<name>/hooks.json PreToolUse/PostToolUse + injectSteps
3. Pluggable statusline statusLine command, JSON on stdin ✗ built-in item list only (tui.status_line); command-statusline is an open FR ⚠️ ctx.ui.setStatus imperative push, not a payload callback ✅✅ /statusline <command>, and it natively carries StatusLineQuotaBucket + StatusLineSubagent
4. Auto-downshift subagent model/effort CLAUDE_CODE_SUBAGENT_MODEL env, at launch ⚠️ subagents exist w/ per-file model/model_reasoning_effort, but no live env lever ✗ no subagents in core (explicit non-goal); per-instance --model/--thinking only ⚠️ subagent fan-out exists but children "inherit the current preset config"; no per-subagent model field

Verified: Codex usage % shape (on disk, 2026-07-16)

Pillar 1 for Codex is confirmed against live session files (~/.codex/sessions/<date>/rollout-*.jsonl) — upgraded from inferred to observed. Each turn appends a rate_limits record:

"rate_limits": {
  "limit_id": "codex",
  "primary":   { "used_percent": 5.0, "window_minutes": 10080, "resets_at": 1784388761 },
  "secondary": null,
  "credits":   { "has_credits": false, "unlimited": false, "balance": null },
  "plan_type": "free",
  "rate_limit_reached_type": null
}

Adapter implications:

  • The % anchor is real and local. primary.{used_percent, resets_at} + window_minutes: 10080 (= 7d) is the direct analogue of Claude's seven_day.{used_percentage, resets_at}, so the pace/pool engine can run on Codex (unlike pi). Field names differ (used_percent not used_percentage; the window is carried as window_minutes, not a named key), so the ingestion adapter maps rather than aliases.
  • primary is nullable (null until the first limit-fetch of a session) — read the newest populated line; it's last-write-wins per turn.
  • secondary exists but was null in every observed session — presumably the shorter window (Claude's five_hour analogue); its populated shape is still unconfirmed (open item below).
  • credits is nested inside rate_limits (not a sibling): {has_credits, unlimited, balance} is a local fallback-credit signal — contrast Antigravity, whose credits are server-side only. So Codex's hard-stop is partly readable offline (has_credits:false + used_percent at 100).
  • rate_limit_reached_type (null until tripped) is the local "which limit did I hit" signal for the error-on-hit path.

Observed live: primary.used_percent climbed 96 → 98 → 100 across turns (written per-turn), then error-on-hit at 100% with has_credits:false — no clean CLI surface short of parsing this file or interactive /status (ref FR #15281).

The $ layer ports (ccusage is multi-source)

ccusage computes API-equivalent USD (pricing from LiteLLM's dataset) for Claude Code, Codex, OpenCode, Amp, Droid, Codebuff, Hermes Agent, pi-agent, Goose, OpenClaw, Kilo, Kimi, Qwen, GitHub Copilot CLI, and Gemini CLI, via per-source subcommands (ccusage codex daily --json, ccusage pi session --json, ccusage gemini daily, …). ccpool's "delegate every dollar to ccusage" invariant holds cross-CLI — same tool, different source.

Two documented caveats keep it from being free:

  1. calib shells ccusage blocks --json, and blocks is Claude-specific. The 5-hour billing-block view exists only for the Claude source; Codex and pi expose only daily / monthly / session (both --json). So internal/calib needs per-source command + result-shape handling, not just a source flag. (Codex source is flagged experimental: "Codex log support is experimental while the Codex CLI log format continues to evolve.")
  2. ccusage gives the $ numerator; the $/1% calibration still needs a host account-% denominator. ccpool's output isn't raw spend — it's $ per 1% of the account-global pool, joining ccusage cost against rate_limits %-deltas. That % still comes from the host (see pillar 1), and pi exposes no account-global % at all (only provider retry-after / anthropic-ratelimit-* headers). So for pi you'd get a dollar figure but nothing to calibrate it into a pool/pace signal — the core verdict engine can't run without a % anchor.

The one thing that is genuinely Claude-only, everywhere

Dynamic subagent downshift (the "coast when ahead of pace" lever). Ports worst across the board: Codex only sets model/effort statically in agent TOML (no runtime env lever); Antigravity subagents inherit the parent preset; pi has no subagents. Claude Code's launch-time CLAUDE_CODE_SUBAGENT_MODEL is genuinely special.

What ports well

The read + warn + statusline loop. Antigravity is the best non-Claude target — its statusline already ingests a quota bucket and subagent state natively, which is exactly ccpool's shape. Codex is close on warn but blocked on statusline (built-in items only).

Architecture caveat that affects effort

  • Codex & agy are config-file + external-command shaped (like Claude Code) → a Go wrapper binary fits; you write per-host adapters for the config format and usage-signal source.
  • pi is different in kind: its hooks/statusline are a TypeScript extension API (pi.on(...), ctx.ui.setStatus), not shell-command hooks. A pi port is not a ccpool adapter — it's a rewrite as a pi extension in TS (different language, different distribution).

Effort read

Target Read % warn Statusline Downshift $ (ccusage) Verdict
Codex ✅ verified JSONL (primary.used_percent) ✅ hooks ✗ blocked (FR only) ⚠️ static-only ✅ ccusage codex (daily/session, no blocks; experimental) ~1 wk warn-only governor w/ $; no statusline, no dynamic downshift
Antigravity ⚠️ backend RPC ✅ hooks ✅ best fit (quota bucket) ⚠️ inherit-only ⚠️ not in ccusage source list; credits are server-side most complete non-Claude port; $ story unclear (server-side credits, not yet a ccusage source)
pi ✗ none (headers only) ✅ (TS extension) ⚠️ setStatus push ✗ no subagents ✅ ccusage pi (daily/monthly/session) $ available but no % anchor to calibrate against → pace/pool engine can't run; also a TS-extension rewrite, not an adapter

Prerequisite in ccpool

There is no provider-abstraction seam today — every coupling reads Claude field names inline. The Claude-specific surface is concentrated in five spots, all isolated:

  1. Ingestion field names — internal/pool/pool.go (rate_limits.{seven_day,five_hour}.{used_percentage,resets_at}), internal/history/history.go, internal/statusline/statusline.go. (Codex analogue, verified: rate_limits.primary.{used_percent,resets_at} + window_minutes; see the verified shape above — the adapter maps names + derives the window tier from window_minutes.)
  2. Hook contract — internal/warn/warn.go (UserPromptSubmit/PostToolUse, hookSpecificOutput.additionalContext).
  3. Downshift lever — internal/run/run.go (CLAUDE_CODE_SUBAGENT_MODEL / CLAUDE_CODE_EFFORT_LEVEL).
  4. File locations — internal/paths/paths.go, internal/initcmd/init.go.
  5. init wiring shape — settings.json schema in internal/initcmd/init.go.

Everything else — profile, burn, runway, report, store, pace/verdict math — is already provider-agnostic. calib is nearly so (it delegates to ccusage, which is multi-source) but hardcodes the Claude-only blocks --json command + blocks[] schema, so it needs a per-source command/shape adapter (ccusage <source> daily|session --json). A port means introducing a Source/host adapter interface at points 1-5 (+ the calib command).

Open questions / possible next steps

  • Decide the actual goal: show other-CLI users their pool (feasible) vs govern their spend the way ccpool governs Claude (not achievable on any host — the two Claude-only levers).
  • Tier-0 refactor: extract a host/Source seam feeding normalized {used%, resets_at, window} tuples (net-neutral, worth doing regardless). Adapters should also declare capabilities (read-%, warn, statusline, downshift) so the governance layer degrades per host ("governance unavailable on this host") rather than branching on host identity or asserting false parity. (Capability-declaration framing via @ofekron / Better Agent.)
  • PoC a Codex or Antigravity read + warn adapter behind that seam, shipped as an explicit --host mode with "governance features unavailable on this host" degradation (no false parity claim). Codex is the strongest first target: hooks (parity), usage-% (JSONL, shape now verified), and $ (ccusage) all present; only statusline + dynamic downshift missing.
  • Confirm Codex's secondary (short-window) populated shape — only primary (weekly, window_minutes:10080) was seen populated; secondary was null in every session observed 2026-07-16, so the five_hour analogue is unproven.
  • Teach calib a per-source ccusage command (ccusage <source> daily|session --json) since blocks is Claude-only.
  • Check whether ccusage has (or plans) an Antigravity source — it isn't in the current 15-source list, and Antigravity bills server-side "AI Credits", so the $ path there is the open question, not the %.
  • Confirm the Antigravity claims against live docs (the antigravity.google/docs/* pages are an Angular SPA; page bodies were not machine-readable — findings were corroborated from the installed agy v1.1.1 binary instead).

Sources

Codex: subagents, config-reference, config-advanced, hooks, slash-commands; FRs #15281 (expose usage), #20043 (command statusline), #16933 (additionalContext reaches model), #23190 (unstable reset window).
pi: earendil-works/pi, coding-agent README, extensions.md.
Antigravity: llms.txt, CLI product, docs/hooks, docs/subagents, docs/cli/statusline, docs/cli/credits; plus the installed agy v1.1.1 binary and local config.
ccusage (multi-source $): all reports / source list, Codex source (beta), pi source (beta), Gemini source, repo.


🤖 Claude-driven investigation (parallel research agents against official docs + locally installed binaries). Findings above are scoping, not verified-in-code parity claims.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions