@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.
@@ -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
- _STORY_ID_PAT = re.compile(r"\b([A-Z]+(?:-[A-Z]+)*-\d+)\b")
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-010: aggregate now sums per-turn usage tokens, so the
365
- # totals in `ue` reflect the whole cycle. Always compute cost at
366
- # list price for cross-account comparabilitysupersedes FIX-060
367
- # which preferred cost_reported_usd as a workaround for
368
- # last-event-only token counts (that root cause is now gone).
369
- cy["cost_list"] = mp.compute_list_cost(
370
- ue.get("model"),
371
- input_tokens=ue.get("input_tokens", 0),
372
- output_tokens=ue.get("output_tokens", 0),
373
- cache_creation_tokens=ue.get("cache_creation_tokens", 0),
374
- cache_read_tokens=ue.get("cache_read_tokens", 0),
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), git log still has the merge
403
- commit `Merge pull request #N from seanyao/loop/cycle-LABEL`. Extract
404
- PR number + story IDs from the merge subject + body so orphan cycles
405
- can be reclassified done instead of permanently '⏵ running'."""
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-", "--format=%H|||%s|||%b<<<END>>>"],
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
- label_re = re.compile(r"loop/cycle-([A-Za-z0-9-]+)")
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>>>"):
@@ -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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.523.1",
3
+ "version": "2026.523.2",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"