playmaker-cli 0.9.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/CHANGELOG.md +25 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/PKG-INFO +17 -8
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/README.md +16 -7
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/pyproject.toml +1 -1
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/lanes.md +10 -8
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/quotas.md +6 -4
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/cli.py +31 -19
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/quotas.py +82 -11
- playmaker_cli-0.10.0/tests/test_quotas_claude.py +197 -0
- playmaker_cli-0.10.0/tests/test_quotas_codex.py +175 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/.gitignore +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/LICENSE +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/SKILL.md +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/agent-gotchas.md +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/commands.md +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/prompt-templates.md +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/review-board.md +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/scripts/review-board.sh +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/__init__.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/__main__.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/__init__.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/agy.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/base.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/claude.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/codex.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/gemini.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/agents/opencode.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/config.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/notify.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/registry.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/state.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/src/playmaker/watcher.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/__init__.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_agy.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_batch.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_binary.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_claude.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_codex.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_no_changes.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_opencode.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_permissions.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_quotas_antigravity.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_quotas_freshness.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_quotas_zai.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_registry.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_skill.py +0 -0
- {playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/tests/test_state.py +0 -0
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`playmaker quotas` shows the two buckets that actually run out first.**
|
|
13
|
+
Codex's `additional_rate_limits[]` carries a separate GPT-5.3-Codex-Spark
|
|
14
|
+
pool with its own 5h and weekly windows, and Anthropic's `limits[]` carries
|
|
15
|
+
a model-scoped weekly next to the all-models one. Both were in the raw
|
|
16
|
+
responses and dropped by the probe, so a coach could route a junior WP to
|
|
17
|
+
Spark while its weekly sat at 4%, or budget a session on the all-models bar
|
|
18
|
+
while its own model-scoped bucket was the one about to end it. Codex now
|
|
19
|
+
renders a `Codex — Spark` sub-block and Claude a `Weekly · <model>` row;
|
|
20
|
+
probes return an additive `blocks` key that `_render_provider` draws for any
|
|
21
|
+
provider.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **The coach no longer tiers Antigravity's Claude models by name.**
|
|
26
|
+
`lanes.md` called `agy`'s Claude-Opus "top-tier Claude judgment on Google's
|
|
27
|
+
pool" and offered it as the reviewer to reach for when the Anthropic bucket
|
|
28
|
+
is precious. That roster trails Anthropic's releases by a generation, and
|
|
29
|
+
the name kept pulling real work onto it. The reference now says to tier by
|
|
30
|
+
the version `agy models` shows, and points the second-strong-reviewer row
|
|
31
|
+
at Gemini Pro.
|
|
32
|
+
|
|
8
33
|
## [0.9.0] - 2026-08-24
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: playmaker-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Playing-coach CLI for orchestrating Claude Code, Codex, Antigravity and opencode sub-agents in parallel.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vladsafedev/playmaker
|
|
6
6
|
Project-URL: Repository, https://github.com/vladsafedev/playmaker
|
|
@@ -391,14 +391,19 @@ stores, no scraping and no browser.
|
|
|
391
391
|
```console
|
|
392
392
|
$ playmaker quotas --refresh
|
|
393
393
|
Claude Max 20x
|
|
394
|
-
Session
|
|
395
|
-
Weekly
|
|
396
|
-
|
|
394
|
+
Session ████████████████░░░░ 80% left resets in 3h 12m
|
|
395
|
+
Weekly ███████████░░░░░░░░░ 55% left resets in 4d 6h
|
|
396
|
+
Weekly · Fable ██████░░░░░░░░░░░░░░ 28% left resets in 4d 6h
|
|
397
|
+
Sonnet ███████████████████░ 95% left resets in 4d 6h
|
|
397
398
|
|
|
398
399
|
Codex Plus
|
|
399
400
|
Session ██████████████████░░ 90% left resets in 1h 40m
|
|
400
401
|
Weekly █████████████░░░░░░░ 65% left resets in 2d 9h
|
|
401
402
|
|
|
403
|
+
Codex — Spark
|
|
404
|
+
Session ████████████████████ 100% left resets in 4h 59m
|
|
405
|
+
Weekly ██░░░░░░░░░░░░░░░░░░ 8% left resets in 5d 22h
|
|
406
|
+
|
|
402
407
|
Antigravity (agy)
|
|
403
408
|
Gemini 5h ████████████████████ 100% left
|
|
404
409
|
Gemini weekly ███████████████████░ 95% left
|
|
@@ -411,12 +416,16 @@ Z.ai (GLM, via opencode) Max
|
|
|
411
416
|
MCP tools ███████████████████░ 99% left resets in 25d 11h
|
|
412
417
|
```
|
|
413
418
|
|
|
414
|
-
The `Weekly` and `Sonnet` rows above are the point: they are
|
|
415
|
-
buckets**. So is every agy row, and
|
|
416
|
-
subtask is choosing which of them to spend.
|
|
419
|
+
The `Weekly`, `Weekly · Fable` and `Sonnet` rows above are the point: they are
|
|
420
|
+
**separate buckets**. So is the `Codex — Spark` block, every agy row, and the
|
|
421
|
+
whole Z.ai block. Routing a subtask is choosing which of them to spend.
|
|
417
422
|
|
|
418
423
|
- **Claude** — OAuth usage API; token from the Claude Code Keychain entry.
|
|
419
|
-
-
|
|
424
|
+
Model-scoped weekly buckets come from the usage API's `limits[]` array and
|
|
425
|
+
print as `Weekly · <model>`.
|
|
426
|
+
- **Codex** — ChatGPT `wham/usage` API; token from `~/.codex/auth.json`. The
|
|
427
|
+
Spark model's own 5-hour and weekly windows come from
|
|
428
|
+
`additional_rate_limits[]` and print as their own `Codex — Spark` block.
|
|
420
429
|
- **Antigravity** — prefers agy's **local daemon**
|
|
421
430
|
(`RetrieveUserQuotaSummary` over its embedded gRPC-web endpoint), which is
|
|
422
431
|
the only source for the full categorized breakdown above. Works whenever any
|
|
@@ -364,14 +364,19 @@ stores, no scraping and no browser.
|
|
|
364
364
|
```console
|
|
365
365
|
$ playmaker quotas --refresh
|
|
366
366
|
Claude Max 20x
|
|
367
|
-
Session
|
|
368
|
-
Weekly
|
|
369
|
-
|
|
367
|
+
Session ████████████████░░░░ 80% left resets in 3h 12m
|
|
368
|
+
Weekly ███████████░░░░░░░░░ 55% left resets in 4d 6h
|
|
369
|
+
Weekly · Fable ██████░░░░░░░░░░░░░░ 28% left resets in 4d 6h
|
|
370
|
+
Sonnet ███████████████████░ 95% left resets in 4d 6h
|
|
370
371
|
|
|
371
372
|
Codex Plus
|
|
372
373
|
Session ██████████████████░░ 90% left resets in 1h 40m
|
|
373
374
|
Weekly █████████████░░░░░░░ 65% left resets in 2d 9h
|
|
374
375
|
|
|
376
|
+
Codex — Spark
|
|
377
|
+
Session ████████████████████ 100% left resets in 4h 59m
|
|
378
|
+
Weekly ██░░░░░░░░░░░░░░░░░░ 8% left resets in 5d 22h
|
|
379
|
+
|
|
375
380
|
Antigravity (agy)
|
|
376
381
|
Gemini 5h ████████████████████ 100% left
|
|
377
382
|
Gemini weekly ███████████████████░ 95% left
|
|
@@ -384,12 +389,16 @@ Z.ai (GLM, via opencode) Max
|
|
|
384
389
|
MCP tools ███████████████████░ 99% left resets in 25d 11h
|
|
385
390
|
```
|
|
386
391
|
|
|
387
|
-
The `Weekly` and `Sonnet` rows above are the point: they are
|
|
388
|
-
buckets**. So is every agy row, and
|
|
389
|
-
subtask is choosing which of them to spend.
|
|
392
|
+
The `Weekly`, `Weekly · Fable` and `Sonnet` rows above are the point: they are
|
|
393
|
+
**separate buckets**. So is the `Codex — Spark` block, every agy row, and the
|
|
394
|
+
whole Z.ai block. Routing a subtask is choosing which of them to spend.
|
|
390
395
|
|
|
391
396
|
- **Claude** — OAuth usage API; token from the Claude Code Keychain entry.
|
|
392
|
-
-
|
|
397
|
+
Model-scoped weekly buckets come from the usage API's `limits[]` array and
|
|
398
|
+
print as `Weekly · <model>`.
|
|
399
|
+
- **Codex** — ChatGPT `wham/usage` API; token from `~/.codex/auth.json`. The
|
|
400
|
+
Spark model's own 5-hour and weekly windows come from
|
|
401
|
+
`additional_rate_limits[]` and print as their own `Codex — Spark` block.
|
|
393
402
|
- **Antigravity** — prefers agy's **local daemon**
|
|
394
403
|
(`RetrieveUserQuotaSummary` over its embedded gRPC-web endpoint), which is
|
|
395
404
|
the only source for the full categorized breakdown above. Works whenever any
|
|
@@ -21,9 +21,11 @@ pays. Four lanes:
|
|
|
21
21
|
4. **External dispatch — `codex` / `agy` / `opencode`.** Each on its own subscription or plan — the
|
|
22
22
|
home for write-heavy parallel implementation that can leave the Anthropic subscription.
|
|
23
23
|
- **`agy` (Antigravity)** carries more than Google models: alongside Gemini Flash and Pro tiers it
|
|
24
|
-
serves **Claude Sonnet/Opus (Thinking)** and a GPT-OSS
|
|
25
|
-
pool
|
|
26
|
-
|
|
24
|
+
serves **Claude Sonnet/Opus (Thinking)** and a GPT-OSS tier. Its Claude runs on *Google's*
|
|
25
|
+
pool and spends none of the Anthropic bucket — but that roster has trailed Anthropic's own
|
|
26
|
+
releases by a generation (Claude 4.6 on agy while Claude 5 ships on Anthropic), so tier it by
|
|
27
|
+
the version `agy models` shows, never by the name. Note the internal split: all Gemini models
|
|
28
|
+
share one bucket, Claude and GPT-OSS share another.
|
|
27
29
|
- **`opencode`** is the widest lane: one CLI over ~75 providers addressed as `provider/model` — a
|
|
28
30
|
GLM coding plan, or a model running locally on this machine, which spends no subscription quota
|
|
29
31
|
at all.
|
|
@@ -40,17 +42,17 @@ against the live roster, failing the dispatch on a stale name.
|
|
|
40
42
|
| is an independent stream to monitor separately | `dispatch claude --model sonnet` | tracked, detached, spares the top bucket |
|
|
41
43
|
| is heavy reasoning only the coach can do | coach | top tier, serial |
|
|
42
44
|
| is write-heavy and can leave Claude | codex / agy / opencode | their own quotas |
|
|
43
|
-
| needs
|
|
45
|
+
| needs a second strong reviewer without touching the Anthropic bucket | `dispatch agy --model <gemini-pro-high>` | near-senior judgment on an uncontended pool |
|
|
44
46
|
| is bulk work with every subscription low | `dispatch opencode --model <plan>/<model>` | a separate plan, untouched by the others |
|
|
45
47
|
| is mechanical and privacy-sensitive, or all quotas spent | `dispatch opencode --model <local>/<model>` | runs on this machine, costs wall-clock only |
|
|
46
48
|
|
|
47
49
|
## Tier-matching
|
|
48
50
|
|
|
49
|
-
- **Architectural / spec judgment / cross-module integration** → top tier (coach,
|
|
50
|
-
|
|
51
|
+
- **Architectural / spec judgment / cross-module integration** → top tier (coach, top-tier Codex,
|
|
52
|
+
Gemini-Pro-high on agy for review and advice rather than implementation).
|
|
51
53
|
- **Pattern-following implementation, scoped CRUD, mechanical refactor, test scaffolding, writing
|
|
52
|
-
inside an existing convention** → mid tier (Claude Sonnet,
|
|
53
|
-
|
|
54
|
+
inside an existing convention** → mid tier (Claude Sonnet, Gemini-Pro-low, mid-tier Codex;
|
|
55
|
+
agy's Claude models only when the roster shows a current version). Most delegated implementation lives here.
|
|
54
56
|
- **Recon, summarization, mechanical loops over many files, normalization** → cheap tier (Flash
|
|
55
57
|
tiers, Haiku, fast Codex modes).
|
|
56
58
|
|
|
@@ -13,15 +13,17 @@ the plan instead of quoting the numbers.
|
|
|
13
13
|
several tiers with independent buckets, and the whole point of pulling quotas is to push work
|
|
14
14
|
*away from* the depleted bucket and *toward* the fresh one.
|
|
15
15
|
|
|
16
|
-
- **Claude:** the
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
- **Claude:** the block shows the account-wide `Weekly` plus one `Weekly · <model>` row per
|
|
17
|
+
model-scoped bucket (e.g. `Weekly · Fable`). The model-scoped row is the one that empties first
|
|
18
|
+
during a long coach session — read it, not just `Weekly`.
|
|
19
19
|
- **Antigravity (`agy`):** one Google pool split by family — `Gemini 5h` / `Gemini weekly` and
|
|
20
20
|
`Claude/GPT 5h` / `Claude/GPT weekly`. All Gemini models share the first; Claude *and* GPT-OSS
|
|
21
21
|
share the second. So one Gemini reviewer plus one agy-Claude reviewer costs one hit in each of two
|
|
22
22
|
separate buckets — the cheapest way to buy two independent opinions. Requires agy's local daemon;
|
|
23
23
|
if the table says "daemon offline" it fell back to a coarse Gemini-only view.
|
|
24
|
-
- **Codex:**
|
|
24
|
+
- **Codex:** the main block is the primary Codex window; `Codex — Spark` is a separate block with
|
|
25
|
+
its own `Session` / `Weekly` rows for the Spark model, the junior Codex lane — route Spark work
|
|
26
|
+
off that block, not off the main one.
|
|
25
27
|
- **opencode:** the quota belongs to the *plan behind the provider*, not to the CLI — it appears
|
|
26
28
|
under that provider (e.g. a GLM coding plan's session and weekly credit windows) and reads
|
|
27
29
|
unsupported without a credential. A dispatch pointed at a **local** model spends nothing and never
|
|
@@ -978,6 +978,25 @@ def quotas(
|
|
|
978
978
|
|
|
979
979
|
|
|
980
980
|
def _render_provider(name: str, info: dict) -> None:
|
|
981
|
+
def render_windows(windows: list[dict]) -> None:
|
|
982
|
+
# Adaptive name column so longer categorized labels (e.g. "Claude/GPT
|
|
983
|
+
# weekly") keep the bars aligned instead of overflowing a fixed width.
|
|
984
|
+
name_width = max(11, *(len(w["name"]) for w in windows))
|
|
985
|
+
for window in windows:
|
|
986
|
+
pct = window.get("pct_left")
|
|
987
|
+
bar = _bar(pct) if isinstance(pct, int) else " " * 20
|
|
988
|
+
line = f" [bold]{window['name']:<{name_width}}[/bold] {bar} {pct}% left"
|
|
989
|
+
right_parts = []
|
|
990
|
+
if window.get("reset_relative"):
|
|
991
|
+
right_parts.append(f"resets in {window['reset_relative']}")
|
|
992
|
+
if window.get("forecast"):
|
|
993
|
+
right_parts.append(window["forecast"])
|
|
994
|
+
if window.get("reserve_pct") is not None:
|
|
995
|
+
right_parts.append(f"{window['reserve_pct']}% in reserve")
|
|
996
|
+
if right_parts:
|
|
997
|
+
line += f" [dim]{' · '.join(right_parts)}[/dim]"
|
|
998
|
+
console.print(line)
|
|
999
|
+
|
|
981
1000
|
status = info.get("status")
|
|
982
1001
|
label_color = {
|
|
983
1002
|
"codex": "blue",
|
|
@@ -1017,27 +1036,13 @@ def _render_provider(name: str, info: dict) -> None:
|
|
|
1017
1036
|
return
|
|
1018
1037
|
|
|
1019
1038
|
windows = info.get("windows") or []
|
|
1039
|
+
blocks = info.get("blocks") or []
|
|
1020
1040
|
if not windows:
|
|
1021
1041
|
console.print(" [dim]no quota windows reported[/dim]")
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
name_width = max(11, *(len(w["name"]) for w in windows))
|
|
1027
|
-
for w in windows:
|
|
1028
|
-
pct = w.get("pct_left")
|
|
1029
|
-
bar = _bar(pct) if isinstance(pct, int) else " " * 20
|
|
1030
|
-
line = f" [bold]{w['name']:<{name_width}}[/bold] {bar} {pct}% left"
|
|
1031
|
-
right_parts = []
|
|
1032
|
-
if w.get("reset_relative"):
|
|
1033
|
-
right_parts.append(f"resets in {w['reset_relative']}")
|
|
1034
|
-
if w.get("forecast"):
|
|
1035
|
-
right_parts.append(w["forecast"])
|
|
1036
|
-
if w.get("reserve_pct") is not None:
|
|
1037
|
-
right_parts.append(f"{w['reserve_pct']}% in reserve")
|
|
1038
|
-
if right_parts:
|
|
1039
|
-
line += f" [dim]{' · '.join(right_parts)}[/dim]"
|
|
1040
|
-
console.print(line)
|
|
1042
|
+
if not blocks:
|
|
1043
|
+
return
|
|
1044
|
+
else:
|
|
1045
|
+
render_windows(windows)
|
|
1041
1046
|
|
|
1042
1047
|
# A local provider has models where the metered ones have tiers; naming them
|
|
1043
1048
|
# saves the coach a round-trip to `ollama list` before it can route here.
|
|
@@ -1059,6 +1064,13 @@ def _render_provider(name: str, info: dict) -> None:
|
|
|
1059
1064
|
f" [bold]{'Extra usage':<11}[/bold] ${used:.2f} / ${limit:.2f} [dim]{util_str}[/dim]"
|
|
1060
1065
|
)
|
|
1061
1066
|
|
|
1067
|
+
for block in blocks:
|
|
1068
|
+
block_windows = block.get("windows") or []
|
|
1069
|
+
console.print()
|
|
1070
|
+
console.print(f"[bold {label_color}]{display} — {block['name']}[/bold {label_color}]")
|
|
1071
|
+
if block_windows:
|
|
1072
|
+
render_windows(block_windows)
|
|
1073
|
+
|
|
1062
1074
|
|
|
1063
1075
|
def _bar(pct: int, width: int = 20) -> str:
|
|
1064
1076
|
pct = max(0, min(100, pct))
|
|
@@ -242,6 +242,23 @@ def codex_probe() -> dict:
|
|
|
242
242
|
raise
|
|
243
243
|
|
|
244
244
|
rate = usage.get("rate_limit") or {}
|
|
245
|
+
reserve = _codex_reserve_from_additional(usage) if rate.get("secondary_window") else None
|
|
246
|
+
windows = _codex_windows(
|
|
247
|
+
rate, reserve_pct=reserve
|
|
248
|
+
)
|
|
249
|
+
spark_windows = _codex_spark_windows(usage)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"status": "ok",
|
|
253
|
+
"account_email": payload.get("email"),
|
|
254
|
+
"tier": _humanize_codex_tier(usage.get("plan_type")),
|
|
255
|
+
"windows": windows,
|
|
256
|
+
"blocks": [{"name": "Spark", "windows": spark_windows}] if spark_windows else [],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _codex_windows(rate: dict, *, reserve_pct: int | None = None) -> list[dict]:
|
|
261
|
+
"""Build the standard Session and Weekly rows for a Codex rate-limit block."""
|
|
245
262
|
primary = rate.get("primary_window") or {}
|
|
246
263
|
secondary = rate.get("secondary_window") or {}
|
|
247
264
|
|
|
@@ -266,10 +283,6 @@ def codex_probe() -> dict:
|
|
|
266
283
|
(win_secs - max(0, (reset_at or 0) - time.time())) if reset_at else 0
|
|
267
284
|
)
|
|
268
285
|
forecast = _forecast_label(used, win_secs, elapsed)
|
|
269
|
-
# "In reserve" — averaged unused weekly across the additional metered
|
|
270
|
-
# rate-limits (CodexBar surfaces this so users see they have headroom
|
|
271
|
-
# in alternative buckets like Codex-Spark).
|
|
272
|
-
reserve = _codex_reserve_from_additional(usage)
|
|
273
286
|
windows.append(
|
|
274
287
|
{
|
|
275
288
|
"name": "Weekly",
|
|
@@ -277,16 +290,28 @@ def codex_probe() -> dict:
|
|
|
277
290
|
"reset_at_iso": _epoch_to_iso(reset_at),
|
|
278
291
|
"reset_relative": _format_relative(reset_at),
|
|
279
292
|
"forecast": forecast,
|
|
280
|
-
"reserve_pct":
|
|
293
|
+
"reserve_pct": reserve_pct,
|
|
281
294
|
}
|
|
282
295
|
)
|
|
283
296
|
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
297
|
+
return windows
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _codex_spark_windows(usage: dict) -> list[dict]:
|
|
301
|
+
"""Return the Codex-Spark rate-limit rows, if the provider reports them."""
|
|
302
|
+
extras = usage.get("additional_rate_limits") or []
|
|
303
|
+
if not isinstance(extras, list):
|
|
304
|
+
return []
|
|
305
|
+
for extra in extras:
|
|
306
|
+
if not isinstance(extra, dict):
|
|
307
|
+
continue
|
|
308
|
+
feature = extra.get("metered_feature")
|
|
309
|
+
limit_name = extra.get("limit_name")
|
|
310
|
+
if feature == "codex_bengalfox" or (
|
|
311
|
+
isinstance(limit_name, str) and "spark" in limit_name.lower()
|
|
312
|
+
):
|
|
313
|
+
return _codex_windows(extra.get("rate_limit") or {})
|
|
314
|
+
return []
|
|
290
315
|
|
|
291
316
|
|
|
292
317
|
def _codex_reserve_from_additional(usage: dict) -> int | None:
|
|
@@ -473,6 +498,52 @@ def claude_probe() -> dict:
|
|
|
473
498
|
}
|
|
474
499
|
)
|
|
475
500
|
|
|
501
|
+
scoped_windows: list[dict] = []
|
|
502
|
+
limits = usage.get("limits")
|
|
503
|
+
if isinstance(limits, list):
|
|
504
|
+
for limit in limits:
|
|
505
|
+
if not isinstance(limit, dict) or limit.get("kind") != "weekly_scoped":
|
|
506
|
+
continue
|
|
507
|
+
percent = limit.get("percent")
|
|
508
|
+
scope = limit.get("scope")
|
|
509
|
+
model = scope.get("model") if isinstance(scope, dict) else None
|
|
510
|
+
display_name = model.get("display_name") if isinstance(model, dict) else None
|
|
511
|
+
if isinstance(percent, bool) or not isinstance(percent, (int, float)):
|
|
512
|
+
continue
|
|
513
|
+
if not isinstance(display_name, str):
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
resets_at = limit.get("resets_at")
|
|
517
|
+
forecast: str | None = None
|
|
518
|
+
if isinstance(resets_at, str):
|
|
519
|
+
try:
|
|
520
|
+
reset_dt = datetime.fromisoformat(resets_at.replace("Z", "+00:00"))
|
|
521
|
+
remaining = reset_dt.timestamp() - now
|
|
522
|
+
elapsed = max(0.0, 7 * 86400 - remaining)
|
|
523
|
+
forecast = _forecast_label(float(percent), 7 * 86400, elapsed)
|
|
524
|
+
except ValueError:
|
|
525
|
+
pass
|
|
526
|
+
scoped_windows.append(
|
|
527
|
+
{
|
|
528
|
+
"name": f"Weekly · {display_name}",
|
|
529
|
+
"pct_left": int(round(100 - float(percent))),
|
|
530
|
+
"reset_at_iso": resets_at,
|
|
531
|
+
"reset_relative": _format_relative(resets_at),
|
|
532
|
+
"forecast": forecast,
|
|
533
|
+
"reserve_pct": None,
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if scoped_windows:
|
|
538
|
+
weekly_index = next(
|
|
539
|
+
(index for index, window in enumerate(windows) if window["name"] == "Weekly"),
|
|
540
|
+
None,
|
|
541
|
+
)
|
|
542
|
+
if weekly_index is None:
|
|
543
|
+
windows.extend(scoped_windows)
|
|
544
|
+
else:
|
|
545
|
+
windows[weekly_index + 1 : weekly_index + 1] = scoped_windows
|
|
546
|
+
|
|
476
547
|
# `extra_usage` is the user's monthly overage allowance — surface only
|
|
477
548
|
# as a top-level field, not as the weekly window's reserve %.
|
|
478
549
|
extra = usage.get("extra_usage") or {}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Claude quota probe coverage for model-scoped weekly limits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
|
+
|
|
13
|
+
import playmaker.quotas as quotas
|
|
14
|
+
|
|
15
|
+
# Captured verbatim from api.anthropic.com/api/oauth/usage on a live Max plan.
|
|
16
|
+
_PAYLOAD = {
|
|
17
|
+
"five_hour": {
|
|
18
|
+
"utilization": 14.0,
|
|
19
|
+
"resets_at": "2026-09-04T19:00:00.208321+00:00",
|
|
20
|
+
"limit_dollars": None,
|
|
21
|
+
"used_dollars": None,
|
|
22
|
+
"remaining_dollars": None,
|
|
23
|
+
"locked_reason": None,
|
|
24
|
+
},
|
|
25
|
+
"seven_day": {
|
|
26
|
+
"utilization": 46.0,
|
|
27
|
+
"resets_at": "2026-09-05T03:00:00.208343+00:00",
|
|
28
|
+
"limit_dollars": None,
|
|
29
|
+
"used_dollars": None,
|
|
30
|
+
"remaining_dollars": None,
|
|
31
|
+
"locked_reason": None,
|
|
32
|
+
},
|
|
33
|
+
"seven_day_oauth_apps": None,
|
|
34
|
+
"seven_day_opus": None,
|
|
35
|
+
"seven_day_sonnet": None,
|
|
36
|
+
"seven_day_cowork": None,
|
|
37
|
+
"seven_day_omelette": None,
|
|
38
|
+
"tangelo": None,
|
|
39
|
+
"iguana_necktie": None,
|
|
40
|
+
"omelette_promotional": None,
|
|
41
|
+
"nimbus_quill": {
|
|
42
|
+
"utilization": 0.0,
|
|
43
|
+
"resets_at": None,
|
|
44
|
+
"limit_dollars": None,
|
|
45
|
+
"used_dollars": None,
|
|
46
|
+
"remaining_dollars": None,
|
|
47
|
+
"locked_reason": None,
|
|
48
|
+
},
|
|
49
|
+
"cinder_cove": None,
|
|
50
|
+
"amber_ladder": None,
|
|
51
|
+
"juniper_tide": None,
|
|
52
|
+
"extra_usage": {
|
|
53
|
+
"is_enabled": False,
|
|
54
|
+
"monthly_limit": 2000,
|
|
55
|
+
"used_credits": 0.0,
|
|
56
|
+
"utilization": 0.0,
|
|
57
|
+
"currency": "USD",
|
|
58
|
+
"decimal_places": 2,
|
|
59
|
+
"disabled_reason": "out_of_credits",
|
|
60
|
+
"user_disabled": False,
|
|
61
|
+
"spend_limit_reached": False,
|
|
62
|
+
"credits_ever_enabled": True,
|
|
63
|
+
"daily": None,
|
|
64
|
+
"weekly": None,
|
|
65
|
+
},
|
|
66
|
+
"limits": [
|
|
67
|
+
{
|
|
68
|
+
"kind": "session",
|
|
69
|
+
"group": "session",
|
|
70
|
+
"percent": 14,
|
|
71
|
+
"severity": "normal",
|
|
72
|
+
"resets_at": "2026-09-04T19:00:00.208321+00:00",
|
|
73
|
+
"scope": None,
|
|
74
|
+
"is_active": False,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"kind": "weekly_all",
|
|
78
|
+
"group": "weekly",
|
|
79
|
+
"percent": 46,
|
|
80
|
+
"severity": "normal",
|
|
81
|
+
"resets_at": "2026-09-05T03:00:00.208343+00:00",
|
|
82
|
+
"scope": None,
|
|
83
|
+
"is_active": False,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"kind": "weekly_scoped",
|
|
87
|
+
"group": "weekly",
|
|
88
|
+
"percent": 72,
|
|
89
|
+
"severity": "normal",
|
|
90
|
+
"resets_at": "2026-09-05T03:00:00.208558+00:00",
|
|
91
|
+
"scope": {
|
|
92
|
+
"model": {
|
|
93
|
+
"id": None,
|
|
94
|
+
"display_name": "Fable",
|
|
95
|
+
},
|
|
96
|
+
"surface": None,
|
|
97
|
+
},
|
|
98
|
+
"is_active": True,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
"spend": {
|
|
102
|
+
"used": {
|
|
103
|
+
"amount_minor": 0,
|
|
104
|
+
"currency": "USD",
|
|
105
|
+
"exponent": 2,
|
|
106
|
+
},
|
|
107
|
+
"limit": {
|
|
108
|
+
"amount_minor": 2000,
|
|
109
|
+
"currency": "USD",
|
|
110
|
+
"exponent": 2,
|
|
111
|
+
},
|
|
112
|
+
"percent": 0,
|
|
113
|
+
"severity": "normal",
|
|
114
|
+
"enabled": False,
|
|
115
|
+
"disabled_reason": "out_of_credits",
|
|
116
|
+
"cap": {
|
|
117
|
+
"money": None,
|
|
118
|
+
"credits": {
|
|
119
|
+
"amount_minor": 2000,
|
|
120
|
+
"exponent": 2,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
"balance": None,
|
|
124
|
+
"auto_reload": None,
|
|
125
|
+
"disclaimer": "Usage credits cover you when you hit your plan limits. [Learn more](https://support.claude.com/articles/12429409)",
|
|
126
|
+
"can_purchase_credits": False,
|
|
127
|
+
"can_toggle": False,
|
|
128
|
+
},
|
|
129
|
+
"member_dashboard_available": False,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _respond(monkeypatch, payload: dict) -> None:
|
|
134
|
+
monkeypatch.setattr(quotas, "_http_json", lambda *args, **kwargs: payload)
|
|
135
|
+
monkeypatch.setattr(
|
|
136
|
+
quotas,
|
|
137
|
+
"_claude_load_keychain",
|
|
138
|
+
lambda: {
|
|
139
|
+
"accessToken": "tok",
|
|
140
|
+
"expiresAt": 4_000_000_000_000,
|
|
141
|
+
"subscriptionType": "max",
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
monkeypatch.setattr(quotas, "_claude_email_from_token", lambda token: None)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_model_scoped_weekly_row_is_placed_after_weekly(monkeypatch) -> None:
|
|
148
|
+
_respond(monkeypatch, _PAYLOAD)
|
|
149
|
+
|
|
150
|
+
result = quotas.claude_probe()
|
|
151
|
+
|
|
152
|
+
assert [(window["name"], window["pct_left"]) for window in result["windows"]] == [
|
|
153
|
+
("Session", 86),
|
|
154
|
+
("Weekly", 54),
|
|
155
|
+
("Weekly · Fable", 28),
|
|
156
|
+
]
|
|
157
|
+
weekly, scoped = result["windows"][1:]
|
|
158
|
+
assert scoped["reset_at_iso"] == _PAYLOAD["limits"][2]["resets_at"]
|
|
159
|
+
assert (scoped["forecast"] is None) == (weekly["forecast"] is None)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_inactive_model_scoped_weekly_limit_is_still_rendered(monkeypatch) -> None:
|
|
163
|
+
payload = deepcopy(_PAYLOAD)
|
|
164
|
+
payload["limits"][2]["is_active"] = False
|
|
165
|
+
_respond(monkeypatch, payload)
|
|
166
|
+
|
|
167
|
+
assert "Weekly · Fable" in [window["name"] for window in quotas.claude_probe()["windows"]]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("shape", ["missing_name", "null_limits", "absent_limits"])
|
|
171
|
+
def test_malformed_or_missing_scoped_limits_are_ignored(monkeypatch, shape: str) -> None:
|
|
172
|
+
payload = deepcopy(_PAYLOAD)
|
|
173
|
+
if shape == "missing_name":
|
|
174
|
+
del payload["limits"][2]["scope"]["model"]["display_name"]
|
|
175
|
+
elif shape == "null_limits":
|
|
176
|
+
payload["limits"] = None
|
|
177
|
+
else:
|
|
178
|
+
del payload["limits"]
|
|
179
|
+
_respond(monkeypatch, payload)
|
|
180
|
+
|
|
181
|
+
assert "Weekly · Fable" not in [window["name"] for window in quotas.claude_probe()["windows"]]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_scoped_rows_precede_existing_model_window(monkeypatch) -> None:
|
|
185
|
+
payload = deepcopy(_PAYLOAD)
|
|
186
|
+
payload["seven_day_sonnet"] = {
|
|
187
|
+
"utilization": 12.0,
|
|
188
|
+
"resets_at": "2026-09-05T03:00:00.208343+00:00",
|
|
189
|
+
}
|
|
190
|
+
_respond(monkeypatch, payload)
|
|
191
|
+
|
|
192
|
+
assert [window["name"] for window in quotas.claude_probe()["windows"]] == [
|
|
193
|
+
"Session",
|
|
194
|
+
"Weekly",
|
|
195
|
+
"Weekly · Fable",
|
|
196
|
+
"Sonnet",
|
|
197
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Codex quota probe coverage for model-scoped Spark limits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
10
|
+
|
|
11
|
+
import playmaker.quotas as quotas
|
|
12
|
+
from playmaker import cli
|
|
13
|
+
|
|
14
|
+
# Captured verbatim from chatgpt.com/backend-api/wham/usage on a live Pro Lite
|
|
15
|
+
# plan. The account identifiers are intentionally redacted.
|
|
16
|
+
_PAYLOAD = {
|
|
17
|
+
"user_id": "<redacted>",
|
|
18
|
+
"account_id": "<redacted>",
|
|
19
|
+
"email": "<redacted>",
|
|
20
|
+
"plan_type": "prolite",
|
|
21
|
+
"rate_limit": {
|
|
22
|
+
"allowed": True,
|
|
23
|
+
"limit_reached": False,
|
|
24
|
+
"primary_window": {
|
|
25
|
+
"used_percent": 34,
|
|
26
|
+
"limit_window_seconds": 604800,
|
|
27
|
+
"reset_after_seconds": 218486,
|
|
28
|
+
"reset_at": 1788751180,
|
|
29
|
+
},
|
|
30
|
+
"secondary_window": None,
|
|
31
|
+
},
|
|
32
|
+
"code_review_rate_limit": None,
|
|
33
|
+
"additional_rate_limits": [
|
|
34
|
+
{
|
|
35
|
+
"limit_name": "GPT-5.3-Codex-Spark",
|
|
36
|
+
"metered_feature": "codex_bengalfox",
|
|
37
|
+
"rate_limit": {
|
|
38
|
+
"allowed": True,
|
|
39
|
+
"limit_reached": False,
|
|
40
|
+
"primary_window": {
|
|
41
|
+
"used_percent": 0,
|
|
42
|
+
"limit_window_seconds": 18000,
|
|
43
|
+
"reset_after_seconds": 18000,
|
|
44
|
+
"reset_at": 1788550695,
|
|
45
|
+
},
|
|
46
|
+
"secondary_window": {
|
|
47
|
+
"used_percent": 92,
|
|
48
|
+
"limit_window_seconds": 604800,
|
|
49
|
+
"reset_after_seconds": 511704,
|
|
50
|
+
"reset_at": 1789044398,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"normal_model_slug": None,
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"model_usage": {},
|
|
57
|
+
"credits": {
|
|
58
|
+
"has_credits": False,
|
|
59
|
+
"unlimited": False,
|
|
60
|
+
"overage_limit_reached": False,
|
|
61
|
+
"balance": "0",
|
|
62
|
+
"approx_local_messages": [0, 0],
|
|
63
|
+
"approx_cloud_messages": [0, 0],
|
|
64
|
+
},
|
|
65
|
+
"spend_control": {
|
|
66
|
+
"reached": False,
|
|
67
|
+
"individual_limit": None,
|
|
68
|
+
},
|
|
69
|
+
"rate_limit_reached_type": None,
|
|
70
|
+
"promo": None,
|
|
71
|
+
"rate_limit_reset_credits": {
|
|
72
|
+
"available_count": 2,
|
|
73
|
+
"applicable_available_count": 0,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _respond(monkeypatch, payload: dict) -> None:
|
|
79
|
+
monkeypatch.setattr(quotas, "_http_json", lambda *args, **kwargs: payload)
|
|
80
|
+
monkeypatch.setattr(
|
|
81
|
+
quotas,
|
|
82
|
+
"_codex_load_auth",
|
|
83
|
+
lambda: {"tokens": {"access_token": "tok"}, "account_id": "acct"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_main_and_spark_windows_are_kept_separate(monkeypatch) -> None:
|
|
88
|
+
_respond(monkeypatch, _PAYLOAD)
|
|
89
|
+
|
|
90
|
+
result = quotas.codex_probe()
|
|
91
|
+
|
|
92
|
+
assert [(window["name"], window["pct_left"]) for window in result["windows"]] == [
|
|
93
|
+
("Session", 66)
|
|
94
|
+
]
|
|
95
|
+
assert result["windows"][0]["reserve_pct"] is None
|
|
96
|
+
assert [(window["name"], window["pct_left"]) for window in result["blocks"][0]["windows"]] == [
|
|
97
|
+
("Session", 100),
|
|
98
|
+
("Weekly", 8),
|
|
99
|
+
]
|
|
100
|
+
spark_session, spark_weekly = result["blocks"][0]["windows"]
|
|
101
|
+
assert spark_session["forecast"] is None
|
|
102
|
+
assert spark_weekly["reset_at_iso"].startswith("2026-09-")
|
|
103
|
+
assert spark_weekly["reset_relative"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_missing_or_empty_additional_limits_have_no_spark_block(monkeypatch) -> None:
|
|
107
|
+
for additional_limits in ([], None):
|
|
108
|
+
payload = deepcopy(_PAYLOAD)
|
|
109
|
+
if additional_limits is None:
|
|
110
|
+
del payload["additional_rate_limits"]
|
|
111
|
+
else:
|
|
112
|
+
payload["additional_rate_limits"] = additional_limits
|
|
113
|
+
_respond(monkeypatch, payload)
|
|
114
|
+
|
|
115
|
+
assert quotas.codex_probe()["blocks"] == []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_non_spark_additional_limit_remains_weekly_reserve(monkeypatch) -> None:
|
|
119
|
+
payload = deepcopy(_PAYLOAD)
|
|
120
|
+
payload["rate_limit"]["secondary_window"] = {
|
|
121
|
+
"used_percent": 25,
|
|
122
|
+
"limit_window_seconds": 604800,
|
|
123
|
+
"reset_after_seconds": 511704,
|
|
124
|
+
"reset_at": 1789044398,
|
|
125
|
+
}
|
|
126
|
+
payload["additional_rate_limits"] = [
|
|
127
|
+
{
|
|
128
|
+
"limit_name": "GPT-5 reserve",
|
|
129
|
+
"metered_feature": "codex_reserve",
|
|
130
|
+
"rate_limit": {
|
|
131
|
+
"secondary_window": {
|
|
132
|
+
"used_percent": 40,
|
|
133
|
+
"limit_window_seconds": 604800,
|
|
134
|
+
"reset_at": 1789044398,
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
_respond(monkeypatch, payload)
|
|
140
|
+
|
|
141
|
+
result = quotas.codex_probe()
|
|
142
|
+
|
|
143
|
+
assert result["blocks"] == []
|
|
144
|
+
assert result["windows"][1]["name"] == "Weekly"
|
|
145
|
+
assert result["windows"][1]["reserve_pct"] == 60
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_rendering_draws_spark_as_a_separate_block(monkeypatch) -> None:
|
|
149
|
+
_respond(monkeypatch, _PAYLOAD)
|
|
150
|
+
result = quotas.codex_probe()
|
|
151
|
+
|
|
152
|
+
with cli.console.capture() as capture:
|
|
153
|
+
cli._render_provider("codex", result)
|
|
154
|
+
text = capture.get()
|
|
155
|
+
|
|
156
|
+
assert text.count("Codex — Spark") == 1
|
|
157
|
+
assert text.index("Codex — Spark") > text.index("Session")
|
|
158
|
+
|
|
159
|
+
with cli.console.capture() as capture:
|
|
160
|
+
cli._render_provider(
|
|
161
|
+
"codex",
|
|
162
|
+
{
|
|
163
|
+
"status": "ok",
|
|
164
|
+
"windows": [
|
|
165
|
+
{
|
|
166
|
+
"name": "Session",
|
|
167
|
+
"pct_left": 66,
|
|
168
|
+
"reset_relative": None,
|
|
169
|
+
"forecast": None,
|
|
170
|
+
"reserve_pct": None,
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
assert "Codex — Spark" not in capture.get()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/agent-gotchas.md
RENAMED
|
File without changes
|
|
File without changes
|
{playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/prompt-templates.md
RENAMED
|
File without changes
|
{playmaker_cli-0.9.0 → playmaker_cli-0.10.0}/skills/playmaker-coach/references/review-board.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|