@seanyao/roll 2026.523.1 → 2026.523.2
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.
- package/CHANGELOG.md +27 -0
- package/bin/roll +418 -14
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/i18n.sh +113 -0
- package/lib/loop-fmt.py +45 -0
- package/lib/model_prices.py +78 -38
- package/lib/prices/snapshot-2026-05-22.json +20 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-loop-status.py +43 -19
- package/lib/roll_render.py +6 -1
- package/package.json +1 -1
package/lib/roll-loop-status.py
CHANGED
|
@@ -153,7 +153,11 @@ def load_backlog(project_root: Optional[Path] = None) -> Dict[str, str]:
|
|
|
153
153
|
# ════════════════════════════════════════════════════════════════════════════
|
|
154
154
|
# Cycle aggregation — group events by cycle label; attach cron + story id
|
|
155
155
|
# ════════════════════════════════════════════════════════════════════════════
|
|
156
|
-
|
|
156
|
+
# FIX-108: each segment was [A-Z]+ (letters only), so alphanumeric segments
|
|
157
|
+
# like I18N / K8S / D2 / S3 / 2FA failed to match — dashboard silently dropped
|
|
158
|
+
# any story id with a mixed-letter-digit segment (US-I18N-001 etc.). First
|
|
159
|
+
# char must still be a letter so "001-002" doesn't false-positive as an id.
|
|
160
|
+
_STORY_ID_PAT = re.compile(r"\b([A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-\d+)\b")
|
|
157
161
|
_PR_NUM_PAT = re.compile(r"/pull/(\d+)")
|
|
158
162
|
|
|
159
163
|
def _extract_story_id(ev_detail: str) -> Optional[str]:
|
|
@@ -361,18 +365,24 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
361
365
|
cy["cache_creation_tokens"] = int(ue.get("cache_creation_tokens") or 0)
|
|
362
366
|
cy["cache_read_tokens"] = int(ue.get("cache_read_tokens") or 0)
|
|
363
367
|
cy["model"] = ue.get("model")
|
|
364
|
-
# US-VIEW-
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
368
|
+
# US-VIEW-014: prefer the cost frozen at cycle_end so a later
|
|
369
|
+
# prices refresh never rewrites a historical cycle's cost. Only
|
|
370
|
+
# legacy events (pre-US-VIEW-014) fall back to recomputing — and
|
|
371
|
+
# the row gets a muted [legacy] tag so it can't be mistaken for
|
|
372
|
+
# the authoritative value.
|
|
373
|
+
persisted = ue.get("cost_list_usd")
|
|
374
|
+
if persisted is not None:
|
|
375
|
+
cy["cost_list"] = float(persisted)
|
|
376
|
+
cy["cost_list_legacy"] = False
|
|
377
|
+
else:
|
|
378
|
+
cy["cost_list"] = mp.compute_list_cost(
|
|
379
|
+
ue.get("model"),
|
|
380
|
+
input_tokens=ue.get("input_tokens", 0),
|
|
381
|
+
output_tokens=ue.get("output_tokens", 0),
|
|
382
|
+
cache_creation_tokens=ue.get("cache_creation_tokens", 0),
|
|
383
|
+
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
384
|
+
)
|
|
385
|
+
cy["cost_list_legacy"] = True
|
|
376
386
|
if ue.get("duration_ms") and not cy.get("duration_s"):
|
|
377
387
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
378
388
|
continue
|
|
@@ -394,25 +404,39 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
394
404
|
cache_creation_tokens=u["cache_creation_tokens"],
|
|
395
405
|
cache_read_tokens=u["cache_read_tokens"],
|
|
396
406
|
)
|
|
407
|
+
# US-VIEW-014: session salvage never has a frozen cycle_end cost, so
|
|
408
|
+
# this path is always legacy.
|
|
409
|
+
cy["cost_list_legacy"] = True
|
|
397
410
|
if u.get("duration_ms") and not cy.get("duration_s"):
|
|
398
411
|
cy["duration_s"] = int(u["duration_ms"] / 1000)
|
|
399
412
|
|
|
400
413
|
def load_pr_merges_from_git(days: int) -> Dict[str, Dict[str, Any]]:
|
|
401
414
|
"""Repair fallback: when events.ndjson dropped the pr / cycle_end events
|
|
402
|
-
for a cycle (events writer regressions
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
415
|
+
for a cycle (events writer regressions, or cycle_end fired before PR
|
|
416
|
+
merged), git log still has the merge commit. Two known subject formats:
|
|
417
|
+
|
|
418
|
+
- Branch-named (Merge commit / older squash): "Merge pull request #N
|
|
419
|
+
from seanyao/loop/cycle-LABEL" — the branch name carries the label.
|
|
420
|
+
- Squash with default-title (newer GitHub UI / `gh pr merge --squash`):
|
|
421
|
+
"loop cycle LABEL (#N)" — space-separated, no slash.
|
|
422
|
+
|
|
423
|
+
FIX-107: the old --grep="loop/cycle-" + label_re missed the squash
|
|
424
|
+
subject entirely, so PRs merged AFTER cycle_end never got their
|
|
425
|
+
pr_outcome promoted to 'merged' on the dashboard.
|
|
426
|
+
"""
|
|
406
427
|
try:
|
|
407
428
|
out = subprocess.check_output(
|
|
408
429
|
["git", "log", f"--since={days + 1} days ago",
|
|
409
|
-
"--grep=loop/cycle
|
|
430
|
+
"--grep=loop[ /]cycle", "--extended-regexp",
|
|
431
|
+
"--format=%H|||%s|||%b<<<END>>>"],
|
|
410
432
|
text=True, errors="ignore"
|
|
411
433
|
)
|
|
412
434
|
except Exception:
|
|
413
435
|
return {}
|
|
414
436
|
result: Dict[str, Dict[str, Any]] = {}
|
|
415
|
-
|
|
437
|
+
# Accept both `loop/cycle-LABEL` and `loop cycle LABEL` (with or without
|
|
438
|
+
# the leading `-` separator after `cycle`). LABEL = YYYYMMDD-HHMMSS-PID.
|
|
439
|
+
label_re = re.compile(r"loop[ /]cycle[-\s](\d{8}-\d+-\d+)")
|
|
416
440
|
pr_re = re.compile(r"#(\d+)")
|
|
417
441
|
story_re = re.compile(r"\b([A-Z]+(?:-[A-Z]+)*-\d+)\b")
|
|
418
442
|
for chunk in out.split("<<<END>>>"):
|
package/lib/roll_render.py
CHANGED
|
@@ -350,6 +350,11 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
350
350
|
"open": ("dim", "…"),
|
|
351
351
|
}.get(pr_outcome, ("dim", "…"))
|
|
352
352
|
pr_marker = " " + c(mark_c, f"#{pr_num} {mark_sym}")
|
|
353
|
+
# US-VIEW-014: pre-US-VIEW-014 events (no frozen cost_list_usd at
|
|
354
|
+
# cycle_end) get a muted [legacy] suffix — the number is recomputed on
|
|
355
|
+
# the fly and can shift with future price changes, unlike the frozen
|
|
356
|
+
# values written by current loop-fmt.
|
|
357
|
+
legacy_marker = " " + c("muted", "[legacy]") if cy.get("cost_list_legacy") else ""
|
|
353
358
|
inner = (
|
|
354
359
|
" " + c(glyph_c, glyph, bold=True) + " " +
|
|
355
360
|
c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
|
|
@@ -357,7 +362,7 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
357
362
|
c("muted", pad(tok, 26)) + " " +
|
|
358
363
|
model_seg +
|
|
359
364
|
c("muted", pad(cost, 7, "r")) + " " +
|
|
360
|
-
c(sid_c, ids_str, bold=True) + pr_marker
|
|
365
|
+
c(sid_c, ids_str, bold=True) + pr_marker + legacy_marker
|
|
361
366
|
)
|
|
362
367
|
# Subtle red bg on failure rows so a fail can't be missed at a glance.
|
|
363
368
|
if outcome == "fail" and USE_COLOR:
|