@seanyao/roll 2026.522.1 → 2026.523.1

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.
@@ -7,7 +7,7 @@
7
7
  sync_claude: ~/.claude/CLAUDE.md
8
8
  sync_kimi: ~/.kimi/AGENTS.md
9
9
  sync_codex: ~/.codex/AGENTS.md
10
- sync_gemini: ~/.gemini/GEMINI.md
10
+ sync_agy: ~/.gemini/GEMINI.md # Antigravity (agy) reuses the legacy ~/.gemini/ path
11
11
 
12
12
  # User preferences
13
13
  default_language: zh
@@ -133,7 +133,7 @@ conventions, deploy targets.
133
133
  |---|---|---|
134
134
  | `conventions/global/AGENTS.md` | `conventions/templates/<type>/AGENTS.md` | All agents |
135
135
  | `conventions/global/CLAUDE.md` | `conventions/templates/<type>/CLAUDE.md` | Claude Code |
136
- | `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` | Gemini CLI |
136
+ | `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` | Antigravity (agy) — reads `~/.gemini/GEMINI.md` natively |
137
137
  | `conventions/global/project_rules.md` | `conventions/templates/<type>/project_rules.md` | Trae IDE |
138
138
 
139
139
  The CLAUDE / GEMINI / project_rules global files themselves declare they
@@ -1,9 +1,14 @@
1
- # Global Preferences — Gemini CLI
1
+ # Global Preferences — Antigravity (agy)
2
2
 
3
3
  > Extends AGENTS.md in this directory — read that first for shared conventions.
4
- > This file adds Gemini CLI-specific configuration only.
4
+ > This file adds Antigravity-specific configuration only.
5
+ >
6
+ > File is named `GEMINI.md` because agy reads `~/.gemini/GEMINI.md` natively
7
+ > (it reuses the legacy Gemini CLI config dir). The filename is the canonical
8
+ > agy-side filename; the content here is for agy.
5
9
 
6
- ## Gemini-Specific
10
+ ## Antigravity-Specific
7
11
 
8
12
  - When running shell commands, prefer the most specific tool available.
9
13
  - When a project has Roll workflow, follow the AGENTS.md conventions and use Roll skills.
14
+ - Invoke interactively with `agy -i "<prompt>"`; non-interactive with `agy -p "<prompt>"`.
@@ -1,6 +1,6 @@
1
- # Project Preferences — Backend Service (Gemini CLI)
1
+ # Project Preferences — Backend Service (Antigravity)
2
2
 
3
- > Extends global GEMINI.md + project AGENTS.md.
3
+ > Extends global GEMINI.md (Antigravity) + project AGENTS.md.
4
4
 
5
5
  ## Stack
6
6
 
@@ -8,7 +8,7 @@
8
8
  - Database: Prisma or Drizzle ORM
9
9
  - Testing: Vitest + Supertest
10
10
 
11
- ## Gemini Notes
11
+ ## Antigravity (agy) Notes
12
12
 
13
13
  - No frontend in this project. API-only service.
14
14
  - Write integration tests that hit real endpoints, not mocked handlers.
@@ -1,6 +1,6 @@
1
- # Project Preferences — CLI Tool (Gemini CLI)
1
+ # Project Preferences — CLI Tool (Antigravity)
2
2
 
3
- > Extends global GEMINI.md + project AGENTS.md.
3
+ > Extends global GEMINI.md (Antigravity) + project AGENTS.md.
4
4
 
5
5
  ## Stack
6
6
 
@@ -8,7 +8,7 @@
8
8
  - CLI framework: commander or citty
9
9
  - Testing: Vitest
10
10
 
11
- ## Gemini Notes
11
+ ## Antigravity (agy) Notes
12
12
 
13
13
  - No server, no frontend. CLI tool only.
14
14
  - Test commands by running them, not just unit tests.
@@ -1,13 +1,13 @@
1
- # Project Preferences — Frontend Only (Gemini CLI)
1
+ # Project Preferences — Frontend Only (Antigravity)
2
2
 
3
- > Extends global GEMINI.md + project AGENTS.md.
3
+ > Extends global GEMINI.md (Antigravity) + project AGENTS.md.
4
4
 
5
5
  ## Stack
6
6
 
7
7
  - React + shadcn/ui + Tailwind CSS + Vite
8
8
  - Testing: Vitest + Playwright
9
9
 
10
- ## Gemini Notes
10
+ ## Antigravity (agy) Notes
11
11
 
12
12
  - No backend in this project. All data via external API consumption.
13
13
  - Run `npm run build` to verify production bundle compiles before pushing.
@@ -1,6 +1,6 @@
1
- # Project Preferences — Fullstack Web (Gemini CLI)
1
+ # Project Preferences — Fullstack Web (Antigravity)
2
2
 
3
- > Extends global GEMINI.md + project AGENTS.md.
3
+ > Extends global GEMINI.md (Antigravity) + project AGENTS.md.
4
4
 
5
5
  ## Stack
6
6
 
@@ -8,7 +8,7 @@
8
8
  - Backend: Node.js API (Express/Hono/Fastify)
9
9
  - Testing: Vitest (unit) + Playwright (E2E)
10
10
 
11
- ## Gemini Notes
11
+ ## Antigravity (agy) Notes
12
12
 
13
13
  - When modifying API contracts, update both `api/types.ts` and `src/shared/types/` in the same commit.
14
14
  - Run `npm run build` to verify both frontend and backend compile before pushing.
package/lib/loop-fmt.py CHANGED
@@ -353,14 +353,28 @@ class LoopFmt:
353
353
  # Use the cumulative totals accumulated across all assistant turns;
354
354
  # result.usage is per-turn (last only) so it would under-count badly.
355
355
  model = result_ev.get("model") or self._last_model or ""
356
+
357
+ # FIX-099: skip writing the usage event when claude returned no real
358
+ # usage data (model empty AND cost/duration both zero). This prevents
359
+ # stale/placeholder values from leaking into the events stream and
360
+ # showing up as "cost=$1.24 dur=372s" in three consecutive cycles when
361
+ # the real cycle had no token data (the default-value fallback).
362
+ # The dashboard can render "n/a" for missing usage rather than false data.
363
+ has_model = bool(model)
364
+ has_tokens = any(self._usage_totals[k] > 0 for k in self._usage_totals)
365
+ has_cost = bool(cost_usd)
366
+ has_dur = bool(dur_ms)
367
+ if not has_model and not has_tokens and not has_cost and not has_dur:
368
+ return # nothing real to report — skip rather than persist zeros
369
+
356
370
  payload = {
357
- "model": model,
371
+ "model": model if has_model else None,
358
372
  "input_tokens": self._usage_totals["input_tokens"],
359
373
  "output_tokens": self._usage_totals["output_tokens"],
360
374
  "cache_creation_tokens": self._usage_totals["cache_creation_tokens"],
361
375
  "cache_read_tokens": self._usage_totals["cache_read_tokens"],
362
- "cost_reported_usd": float(cost_usd or 0),
363
- "duration_ms": int(dur_ms or 0),
376
+ "cost_reported_usd": float(cost_usd) if has_cost else None,
377
+ "duration_ms": int(dur_ms) if has_dur else None,
364
378
  }
365
379
  evfile = os.path.join(shared, "loop", f"events-{slug}.ndjson")
366
380
  line = json.dumps({
@@ -12,15 +12,26 @@ to sonnet rates with a stderr warning so dashboards don't blank out.
12
12
  import sys
13
13
  from typing import Dict, Optional
14
14
 
15
- # Rates per million tokens (USD).
15
+ # Rates per million tokens (USD). cache_create = 5-minute cache write (1.25x
16
+ # input). 1-hour cache writes (2x input) are not modeled — Roll loop uses the
17
+ # default 5m caching only.
18
+ # Source: https://platform.claude.com/docs/en/about-claude/pricing
16
19
  PRICES: Dict[str, Dict[str, float]] = {
17
- # Claude 4.x family (current as of 2026-05).
18
- "claude-opus-4-7": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
19
- "claude-opus-4-6": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
20
+ # Claude 4.x Opus family 2026-05 repricing: Opus 4.5+ moved to
21
+ # $5/$25 base, 3x cheaper than Opus 4 / 4.1.
22
+ "claude-opus-4-7": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
23
+ "claude-opus-4-6": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
24
+ "claude-opus-4-5": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
25
+ "claude-opus-4-1": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
26
+ "claude-opus-4": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
27
+ # Claude 4.x Sonnet family.
20
28
  "claude-sonnet-4-6": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
29
+ "claude-sonnet-4-5": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
21
30
  "claude-sonnet-4": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
31
+ # Claude 4.x Haiku family.
22
32
  "claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_create": 1.25, "cache_read": 0.10},
23
- # Older fallbacks
33
+ # Older / retired models (Bedrock & Vertex only for 3.5 Haiku).
34
+ "claude-haiku-3-5": {"in": 0.80, "out": 4.00, "cache_create": 1.00, "cache_read": 0.08},
24
35
  "claude-3-5-sonnet": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
25
36
  }
26
37
 
@@ -336,10 +336,16 @@ def load_claude_session_usage(label: str, slug: str) -> Optional[Dict[str, Any]]
336
336
  "cost_reported_usd": cost, "duration_ms": duration_ms}
337
337
 
338
338
  def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str) -> None:
339
- """Populate cy['tokens'], cy['cost_list'], cy['model']. Two paths:
339
+ """Populate cy['input_tokens'], cy['output_tokens'], cy['cost_list'],
340
+ cy['model']. Two paths:
340
341
  1. usage_event from events stream (US-LOOP-004 writer side) — authoritative
341
342
  2. claude session JSONL backfill — for cycles that ran before the
342
343
  writer existed, or on machines where events.ndjson got truncated
344
+
345
+ US-VIEW-012: dashboard exposes input + output only (the model's actual
346
+ work). cache_creation / cache_read remain in the usage_event for
347
+ compute_list_cost — they're still part of true API cost — but no longer
348
+ surface in the UI where they previously inflated visible token totals.
343
349
  """
344
350
  import importlib.util
345
351
  spec = importlib.util.spec_from_file_location("model_prices",
@@ -350,12 +356,10 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
350
356
  # Path 1: usage event written by loop-fmt at result time.
351
357
  ue = cy.get("usage_event")
352
358
  if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")):
353
- cy["tokens"] = mp.total_tokens(
354
- input_tokens=ue.get("input_tokens", 0),
355
- output_tokens=ue.get("output_tokens", 0),
356
- cache_creation_tokens=ue.get("cache_creation_tokens", 0),
357
- cache_read_tokens=ue.get("cache_read_tokens", 0),
358
- )
359
+ cy["input_tokens"] = int(ue.get("input_tokens") or 0)
360
+ cy["output_tokens"] = int(ue.get("output_tokens") or 0)
361
+ cy["cache_creation_tokens"] = int(ue.get("cache_creation_tokens") or 0)
362
+ cy["cache_read_tokens"] = int(ue.get("cache_read_tokens") or 0)
359
363
  cy["model"] = ue.get("model")
360
364
  # US-VIEW-010: aggregate now sums per-turn usage tokens, so the
361
365
  # totals in `ue` reflect the whole cycle. Always compute cost at
@@ -373,17 +377,15 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
373
377
  cy["duration_s"] = int(ue["duration_ms"] / 1000)
374
378
  continue
375
379
  # Path 2: salvage from claude's own session log.
376
- if cy.get("tokens"):
380
+ if cy.get("input_tokens") or cy.get("output_tokens"):
377
381
  continue
378
382
  u = load_claude_session_usage(cy.get("label", ""), slug)
379
383
  if not u:
380
384
  continue
381
- cy["tokens"] = mp.total_tokens(
382
- input_tokens=u["input_tokens"],
383
- output_tokens=u["output_tokens"],
384
- cache_creation_tokens=u["cache_creation_tokens"],
385
- cache_read_tokens=u["cache_read_tokens"],
386
- )
385
+ cy["input_tokens"] = int(u.get("input_tokens") or 0)
386
+ cy["output_tokens"] = int(u.get("output_tokens") or 0)
387
+ cy["cache_creation_tokens"] = int(u.get("cache_creation_tokens") or 0)
388
+ cy["cache_read_tokens"] = int(u.get("cache_read_tokens") or 0)
387
389
  cy["model"] = u["model"]
388
390
  cy["cost_list"] = mp.compute_list_cost(
389
391
  u["model"],
@@ -553,15 +555,27 @@ def bucket_by_day(cycles: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]
553
555
  return out
554
556
 
555
557
  def rollup_for_day(day_cycles: List[Dict[str, Any]]) -> Dict[str, Any]:
558
+ # US-VIEW-012: track input + output separately so the daily summary can
559
+ # show two metric rows. cache_read tokens deliberately excluded — they're
560
+ # already captured in cy["cost_list"] via list-price math (compute_list_cost
561
+ # reads all 4 fields), but they don't represent the model's actual work.
556
562
  r = {"cycles": len(day_cycles), "prs": 0, "failed": 0,
557
- "duration_s": 0, "cost": 0.0, "tokens": 0}
563
+ "duration_s": 0, "cost": 0.0,
564
+ "input_tokens": 0, "output_tokens": 0,
565
+ "cache_creation_tokens": 0, "cache_read_tokens": 0}
558
566
  for cy in day_cycles:
559
567
  if cy.get("outcome") == "fail":
560
568
  r["failed"] += 1
561
569
  if cy.get("duration_s"):
562
570
  r["duration_s"] += cy["duration_s"]
563
- if cy.get("tokens"):
564
- r["tokens"] += cy["tokens"]
571
+ if cy.get("input_tokens"):
572
+ r["input_tokens"] += cy["input_tokens"]
573
+ if cy.get("output_tokens"):
574
+ r["output_tokens"] += cy["output_tokens"]
575
+ if cy.get("cache_creation_tokens"):
576
+ r["cache_creation_tokens"] += cy["cache_creation_tokens"]
577
+ if cy.get("cache_read_tokens"):
578
+ r["cache_read_tokens"] += cy["cache_read_tokens"]
565
579
  # US-VIEW-011: rollup only counts cycles whose PR actually merged.
566
580
  # Backward compat: rows where pr_outcome is missing but pr URL exists
567
581
  # (no `pr` event after the writer upgrade ran for that cycle) are
@@ -620,9 +634,26 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
620
634
  c("muted", " · ") + c("dim", state.get("paused_reason", "")))
621
635
  eb_zh = c("dim", " 已暂停 · run: roll loop resume")
622
636
  else:
623
- eb_l = (c("blue", "● IDLE", bold=True) + c("muted", " ") +
624
- c("dim", "next run ") + c("fg", _next_cron_hint(state), bold=True))
625
- eb_zh = c("dim", f" 闲置 · 距下一轮 {_next_cron_hint(state, zh=True)}")
637
+ # FIX-095: surface three-state install/enable status. Pre-FIX, every
638
+ # case fell through to '● IDLE' which hid 'not installed' and
639
+ # 'installed/off' from the user.
640
+ install_state = _detect_install_state()
641
+ if install_state == "not-installed":
642
+ eb_l = (c("muted", "○ not installed", bold=True) + c("muted", " ") +
643
+ c("dim", "run ") + c("fg", "roll loop on", bold=True) +
644
+ c("dim", " to enable"))
645
+ eb_zh = c("dim", " 未安装 · 运行 ") + c("fg", "roll loop on") + c("dim", " 启用")
646
+ elif install_state in ("stale", "disabled"):
647
+ # FIX-098: 'stale' = plist on disk but agent not registered in launchd.
648
+ # 'disabled' kept for back-compat (old install_state values). Both mean
649
+ # the user needs to run 'roll loop on' to bootstrap the agent.
650
+ eb_l = (c("amber", "◌ STALE — plist present, not loaded", bold=True) + c("muted", " ") +
651
+ c("dim", "run ") + c("fg", "roll loop on", bold=True) + c("dim", " to repair"))
652
+ eb_zh = c("dim", " Plist 存在但未加载 · 运行 ") + c("fg", "roll loop on") + c("dim", " 修复")
653
+ else:
654
+ eb_l = (c("blue", "● IDLE", bold=True) + c("muted", " · ") +
655
+ c("dim", "enabled · next run ") + c("fg", _next_cron_hint(state), bold=True))
656
+ eb_zh = c("dim", f" 已启用 · 闲置 · 距下一轮 {_next_cron_hint(state, zh=True)}")
626
657
 
627
658
  # 'last' = the most recent cycle the user can act on — skip cycles that
628
659
  # are still running (the running banner already announces those) and skip
@@ -704,7 +735,13 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
704
735
  yest_color="amber" if yest["failed"] > 0 else "dim",
705
736
  yest_suffix="⚠" if yest["failed"] > 0 else "")
706
737
  metric_dur("duration", today["duration_s"], yest["duration_s"], d2["duration_s"], partial=is_partial)
707
- metric_tokens("tokens", today["tokens"], yest["tokens"], d2["tokens"], partial=is_partial)
738
+ # US-VIEW-017: show all 4 token components so the cost is explainable.
739
+ # cache_creation (↑) and cache_read (↓) typically account for 80-90% of
740
+ # cost — hiding them makes the cost line incomprehensible.
741
+ metric_tokens("input tokens", today["input_tokens"], yest["input_tokens"], d2["input_tokens"], partial=is_partial)
742
+ metric_tokens("cache writes", today["cache_creation_tokens"], yest["cache_creation_tokens"], d2["cache_creation_tokens"], partial=is_partial)
743
+ metric_tokens("cache reads", today["cache_read_tokens"], yest["cache_read_tokens"], d2["cache_read_tokens"], partial=is_partial)
744
+ metric_tokens("output tokens", today["output_tokens"], yest["output_tokens"], d2["output_tokens"], partial=is_partial)
708
745
  metric_dollar("cost", today["cost"], yest["cost"], d2["cost"], partial=is_partial)
709
746
 
710
747
  print()
@@ -759,6 +796,40 @@ def _read_plist_loop_minute() -> int:
759
796
  return int(m.group(1)) if m else 48
760
797
 
761
798
 
799
+ def _detect_install_state() -> str:
800
+ """FIX-095 / FIX-098: classify the launchd install state of the loop service.
801
+
802
+ Returns one of:
803
+ 'not-installed' — no plist for com.roll.loop.<slug> in ~/Library/LaunchAgents/
804
+ 'stale' — plist on disk but agent NOT registered in launchd
805
+ (happens after roll loop off + roll update without roll loop on)
806
+ 'enabled' — plist on disk AND registered in launchd
807
+
808
+ FIX-098: switched from `launchctl print-disabled` (disabled-overrides DB) to
809
+ `launchctl print gui/<uid>/<label>` which probes the actual launchd registry.
810
+ The old approach returned false-positive 'enabled' when the disabled-overrides
811
+ DB had no entry for the label (empty = not explicitly disabled, not loaded).
812
+ """
813
+ slug = project_slug()
814
+ label = f"com.roll.loop.{slug}"
815
+ plist = Path(os.path.expanduser("~/Library/LaunchAgents")) / f"{label}.plist"
816
+ if not plist.exists():
817
+ return "not-installed"
818
+ try:
819
+ uid = os.getuid()
820
+ result = subprocess.run(
821
+ ["launchctl", "print", f"gui/{uid}/{label}"],
822
+ capture_output=True, timeout=2,
823
+ )
824
+ if result.returncode == 0:
825
+ return "enabled"
826
+ return "stale"
827
+ except Exception:
828
+ # launchctl missing or timed out — assume stale (safe: user sees STALE
829
+ # banner and is told to run 'roll loop on' to repair).
830
+ return "stale"
831
+
832
+
762
833
  def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
763
834
  """Compute next cron fire time from the actual launchd plist Minute (FIX-063)."""
764
835
  now = datetime.now().astimezone()
package/lib/roll-peer.py CHANGED
@@ -30,7 +30,7 @@ _AGENT_COLOR = {
30
30
  "codex": "pink",
31
31
  "kimi": "amber",
32
32
  "deepseek": "green",
33
- "gemini": "purple",
33
+ "agy": "purple", # Antigravity (formerly Gemini CLI)
34
34
  "pi": "yellow",
35
35
  "opencode": "muted",
36
36
  "trae": "fg",
@@ -160,7 +160,7 @@ def _fixture_data() -> Dict[str, Any]:
160
160
  ai_clients=[
161
161
  {"name": "claude", "cfg_file": "CLAUDE.md", "path": "~/.claude/CLAUDE.md", "sync": "sync", "skills": 12},
162
162
  {"name": "cursor", "cfg_file": "AGENTS.md", "path": "~/.cursor/AGENTS.md", "sync": "out-of-sync", "skills": 12},
163
- {"name": "gemini", "cfg_file": "GEMINI.md", "path": "~/.gemini/GEMINI.md", "sync": "missing", "skills": 0},
163
+ {"name": "agy", "cfg_file": "GEMINI.md", "path": "~/.gemini/GEMINI.md", "sync": "missing", "skills": 0},
164
164
  ],
165
165
  templates=[
166
166
  ("fullstack", 14), ("frontend-only", 9), ("backend-service", 11), ("cli", 7),
@@ -159,7 +159,8 @@ def trunc(s: str, n: int) -> str:
159
159
  return out
160
160
 
161
161
  def empty_rollup() -> Dict[str, Any]:
162
- return {"cycles": 0, "prs": 0, "failed": 0, "duration_s": 0, "cost": 0.0, "tokens": 0}
162
+ return {"cycles": 0, "prs": 0, "failed": 0, "duration_s": 0, "cost": 0.0,
163
+ "input_tokens": 0, "output_tokens": 0}
163
164
 
164
165
  # ════════════════════════════════════════════════════════════════════════════
165
166
  # Section / metric / cycle rows — printers used by all dashboards
@@ -297,7 +298,19 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
297
298
  from datetime import datetime as _dt, timezone as _tz
298
299
  dur_s = int((_dt.now(_tz.utc) - cy["start"]).total_seconds())
299
300
  dur = fmt_dur(dur_s) if dur_s else "—"
300
- tok = fmt_tokens(cy.get("tokens") or 0)
301
+ # US-VIEW-017: show all 4 token components when cache data is available.
302
+ # Format: "in/cw↑ cr↓/out" (cache writes ↑, cache reads ↓).
303
+ # Falls back to "in/out" for cycles that predate cache tracking.
304
+ inp = cy.get('input_tokens') or 0
305
+ out_tok = cy.get('output_tokens') or 0
306
+ cw = cy.get('cache_creation_tokens') or 0
307
+ cr = cy.get('cache_read_tokens') or 0
308
+ if cw or cr:
309
+ tok = (f"{fmt_tokens(inp)}"
310
+ f"/{fmt_tokens(cw)}↑ {fmt_tokens(cr)}↓"
311
+ f"/{fmt_tokens(out_tok)}")
312
+ else:
313
+ tok = f"{fmt_tokens(inp)}/{fmt_tokens(out_tok)}"
301
314
  # cost prefers the backfilled list-price; falls back to cron.log when
302
315
  # the claude session log isn't available (only the latest cycle).
303
316
  if cy.get("cost_list") is not None:
@@ -341,7 +354,7 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
341
354
  " " + c(glyph_c, glyph, bold=True) + " " +
342
355
  c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
343
356
  c("muted", pad(dur, 4, "r")) + " " +
344
- c("muted", pad(tok, 6, "r")) + " " +
357
+ c("muted", pad(tok, 26)) + " " +
345
358
  model_seg +
346
359
  c("muted", pad(cost, 7, "r")) + " " +
347
360
  c(sid_c, ids_str, bold=True) + pr_marker