@seanyao/roll 2026.520.1 → 2026.521.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.521.2
4
+
5
+ ### Fixed
6
+
7
+ - **`roll setup` 不再在 macOS 后台活动列表里留下 ghost「bash」条目** — 改由 `roll init` 和 `roll loop on` 按需安装 `[loop]`
8
+ - **`roll setup` 现在能正确显示哪些步骤真的装了** — 之前装好的 skills 和 peer 状态目录会被误标成"跳过" `[loop]`
9
+ - **`roll loop status --days 7` 不再吞掉天数参数** — 之前永远只显示默认 3 天,dashboard 底部那条 "more" 提示也改成能直接复制跑通 `[loop]`
10
+ - **loop 每轮起手不再刷一堆"找不到文件"报错** — 修了内部脚本里的旧路径,并让 agent 在 zsh 下不再被未匹配的通配符卡住 `[loop]`
11
+
12
+ ## v2026.521.1
13
+
14
+ ### Added
15
+
16
+ - **dashboard cycle 行现在标出模型和按公开单价算的成本** — 跨账号 / 跨项目可以横向对比和加总,不再被订阅折扣藏掉真实开销 `[loop]`
17
+
18
+ ### Improved
19
+
20
+ - **`roll setup` 重跑能看出"已是最新"还是"刷新了 X 项"** — 每步上报 changed / unchanged / failed,强制覆盖用 `~` 标出 `[loop]`
21
+ - **`roll-peer` 评审第一轮要先独立判断** — 不再被评审方预设结论带跑,跨 agent 才真的是二次判断 `[loop]`
22
+
23
+ ### Fixed
24
+
25
+ - **`roll init` 默认不再打"Project ready"假提示** — 老项目进引导分支时不再骗你说已就绪 `[legacy-onboard]`
26
+
3
27
  ## v2026.520.1
4
28
 
5
29
  ### Added
package/bin/roll CHANGED
@@ -4,7 +4,7 @@ set -euo pipefail
4
4
  # Roll — AI Agent Convention Manager
5
5
  # Single source of truth for how all AI coding agents behave.
6
6
 
7
- VERSION="2026.520.1"
7
+ VERSION="2026.521.2"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -572,53 +572,161 @@ _ensure_tmux() {
572
572
  return 0
573
573
  }
574
574
 
575
+ # FIX-075: snapshot the content of watched directories so cmd_setup can detect
576
+ # whether a step actually changed any file. Uses `cksum` (mtime-independent) so
577
+ # a re-copy with identical content is recognised as a no-op even when the inner
578
+ # helper rewrites the file. Watch is a colon-separated list of directories;
579
+ # missing dirs are skipped silently.
580
+ # FIX-079: also track symlinks (`_link_skills` only creates symlinks) and
581
+ # directories (`_peer_ensure_state_dir` only creates dirs). Without these, a
582
+ # step that did real work but produced no regular file would falsely render
583
+ # as ↷ on a brand-new install.
584
+ _setup_snapshot() {
585
+ local watch="$1"
586
+ local -a dirs
587
+ IFS=':' read -r -a dirs <<<"$watch"
588
+ local d
589
+ {
590
+ for d in "${dirs[@]}"; do
591
+ [[ -d "$d" ]] || continue
592
+ find "$d" -type f -print0 2>/dev/null | xargs -0 cksum 2>/dev/null
593
+ while IFS= read -r l; do
594
+ printf 'L %s -> %s\n' "$l" "$(readlink "$l")"
595
+ done < <(find "$d" -type l 2>/dev/null)
596
+ find "$d" -type d -print 2>/dev/null
597
+ done
598
+ } | sort
599
+ }
600
+
601
+ # FIX-075: run a setup step and report changed/unchanged/failed via the global
602
+ # _ROLL_SETUP_STATE. Caller passes the watch dir(s) plus the command + args.
603
+ # stdout/stderr of the inner command are suppressed (same as the previous
604
+ # pattern in cmd_setup) to keep the v2 UI render the only user-visible output.
605
+ _run_setup_step() {
606
+ local watch="$1"; shift
607
+ local before after
608
+ before=$(_setup_snapshot "$watch")
609
+ if "$@" >/dev/null 2>&1; then
610
+ after=$(_setup_snapshot "$watch")
611
+ if [[ "$before" == "$after" ]]; then
612
+ _ROLL_SETUP_STATE="unchanged"
613
+ else
614
+ _ROLL_SETUP_STATE="changed"
615
+ fi
616
+ else
617
+ _ROLL_SETUP_STATE="failed"
618
+ fi
619
+ }
620
+
575
621
  cmd_setup() {
576
622
  local force=false
577
- local demo=false
578
623
  while [[ $# -gt 0 ]]; do
579
624
  case "$1" in
580
625
  --force|-f) force=true; shift ;;
581
- --demo) demo=true; shift ;;
582
626
  *) err "Unknown argument: $1 未知参数: $1"; exit 1 ;;
583
627
  esac
584
628
  done
585
629
 
586
- if [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; then
587
- python3 "${ROLL_PKG_DIR}/lib/roll-setup.py" --demo
588
- if [[ "$demo" == "true" ]]; then return; fi
589
- fi
630
+ # Capture per-step outcomes for the v2 UI render at the end.
631
+ local steps_buf=()
632
+ _record() { steps_buf+=("$1|$2"); }
633
+
634
+ # Map snapshot-detected state to v2 UI marker. -f rewrites "changed" to
635
+ # "forced" so the user can tell a forced reinstall apart from a fresh
636
+ # install — both produce diff'd files, only -f was explicitly requested.
637
+ _state_to_marker() {
638
+ local s="$1"
639
+ case "$s" in
640
+ changed) [[ "$force" == "true" ]] && echo forced || echo ok ;;
641
+ unchanged) echo skip ;;
642
+ failed) echo fail ;;
643
+ *) echo fail ;;
644
+ esac
645
+ }
590
646
 
591
- info "Setting up Roll on this machine... 正在初始化 Roll..."
592
- echo ""
647
+ local _ai_dirs="$HOME/.claude:$HOME/.gemini:$HOME/.kimi:$HOME/.codex:$HOME/.cursor:$HOME/.trae:$HOME/.config/opencode:$HOME/.openclaw:$HOME/.pi:$HOME/.deepseek"
593
648
 
594
- _install_local "$force"
595
- echo ""
596
- info "Syncing to AI tools... 正在同步到 AI 工具..."
597
- _sync_conventions "$force"
598
- echo ""
599
- _sync_skills "$force"
649
+ _run_setup_step "$ROLL_HOME" _install_local "$force"
650
+ _record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install templates & conventions to ~/.roll"
600
651
 
601
- echo ""
602
- info "Setting up peer review state... 正在初始化 peer review 状态..."
603
- _peer_ensure_state_dir
604
- ok "Peer state directory ready. Peer 状态目录已就绪。"
652
+ _run_setup_step "$_ai_dirs" _sync_conventions "$force"
653
+ _record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Sync conventions to AI tools"
605
654
 
606
- echo ""
607
- info "Ensuring tmux is installed (required for visible loop runs)... 正在确认 tmux 已安装..."
608
- _ensure_tmux
655
+ _run_setup_step "$_ai_dirs" _sync_skills "$force"
656
+ _record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install skills to ~/.claude"
609
657
 
610
- if [[ "$(uname)" == "Darwin" ]]; then
611
- echo ""
612
- info "Installing launchd plists... 正在安装 LaunchAgents..."
613
- _install_launchd_plists "$(pwd -P)"
658
+ _run_setup_step "$ROLL_HOME/.peer-state" _peer_ensure_state_dir
659
+ _record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Initialize peer-review state directory"
660
+
661
+ if command -v tmux >/dev/null 2>&1; then
662
+ _record skip "Ensure tmux is installed (already present)"
663
+ else
664
+ if _ensure_tmux >/dev/null 2>&1 && command -v tmux >/dev/null 2>&1; then
665
+ _record ok "Ensure tmux is installed"
666
+ else
667
+ _record fail "Ensure tmux is installed"
668
+ fi
614
669
  fi
615
670
 
616
- echo ""
617
- ok "Setup complete. 初始化完成。"
671
+ # FIX-078: launchd plist 安装从 setup 里拿掉——plist 是 per-project 资源,
672
+ # setup 是全局安装阶段,不应该给 cwd 留 disabled 的占位。需要时 cmd_init /
673
+ # _loop_on 各自会调 _install_launchd_plists。
618
674
 
619
- echo ""
620
- info "Next: run ${BOLD}roll init${NC} inside a project to initialize it. 下一步:在项目目录运行 roll init"
621
- info "Optional gates: run ${BOLD}roll doctor${NC} inside a repo to see which PR review extras are still off. 可选闸门:在仓库内运行 roll doctor 查看 PR 评审两档开关状态"
675
+ _emit_setup_v2_ui "${steps_buf[@]}"
676
+ }
677
+
678
+ # FIX-073: Render the cmd_setup v2 UI from per-step outcomes captured above.
679
+ # FIX-075: footer composition depends on how many steps actually changed —
680
+ # all unchanged → "no changes"; some forced (~) → "re-installed (forced)";
681
+ # any failed → "Setup incomplete"; otherwise → "X items refreshed".
682
+ _emit_setup_v2_ui() {
683
+ local color_flag=""
684
+ if [[ -n "${NO_COLOR:-}" ]] || ! [ -t 1 ]; then
685
+ color_flag="--no-color"
686
+ fi
687
+
688
+ python3 - "$@" <<'PY' \
689
+ | python3 "${ROLL_PKG_DIR}/lib/roll-setup.py" $color_flag
690
+ import json, sys
691
+ entries = sys.argv[1:]
692
+ steps = []
693
+ for i, entry in enumerate(entries, start=1):
694
+ status, _sep, label = entry.partition("|")
695
+ steps.append({"num": i, "label": label, "status": status})
696
+
697
+ n_failed = sum(1 for s in steps if s["status"] == "fail")
698
+ n_forced = sum(1 for s in steps if s["status"] == "forced")
699
+ n_changed = sum(1 for s in steps if s["status"] == "ok")
700
+
701
+ if n_failed:
702
+ footer_status = "fail"
703
+ label = "Setup incomplete"
704
+ hint = None
705
+ elif n_forced:
706
+ footer_status = "ok"
707
+ label = f"Setup re-installed (forced — {n_forced} item{'s' if n_forced != 1 else ''})"
708
+ hint = "run roll init inside a project"
709
+ elif n_changed == 0:
710
+ footer_status = "ok"
711
+ label = "Setup complete (no changes)"
712
+ hint = "everything already up to date"
713
+ else:
714
+ footer_status = "ok"
715
+ label = f"Setup complete ({n_changed} item{'s' if n_changed != 1 else ''} refreshed)"
716
+ hint = "run roll init inside a project"
717
+
718
+ payload = {
719
+ "header_label": "SETUP",
720
+ "subtitle": "初始化",
721
+ "steps": steps,
722
+ "footer": {
723
+ "status": footer_status,
724
+ "label": label,
725
+ "hint": hint,
726
+ },
727
+ }
728
+ print(json.dumps(payload))
729
+ PY
622
730
  }
623
731
 
624
732
  # ─── PR pipeline hint ────────────────────────────────────────────────────────
@@ -917,21 +1025,20 @@ _merge_claude_to_project() {
917
1025
  # Existing AGENTS.md: re-merges global conventions (section-level, non-destructive)
918
1026
  # ═══════════════════════════════════════════════════════════════════════════════
919
1027
  cmd_init() {
920
- # US-VIEW-008: parse --demo before any side effects so the v2 renderer can
921
- # run standalone (no templates, no project mutation).
922
- local demo=false
923
- local args=()
924
- while [[ $# -gt 0 ]]; do
925
- case "$1" in
926
- --demo) demo=true; shift ;;
927
- *) args+=("$1"); shift ;;
928
- esac
929
- done
930
- set -- "${args[@]:-}"
1028
+ # US-ONBOARD-009: --apply consumes onboard-plan.yaml produced by $roll-onboard
1029
+ if [[ "${1:-}" == "--apply" ]]; then
1030
+ if [[ ! -d "$ROLL_TEMPLATES" ]]; then
1031
+ err "No templates found. Run 'roll setup' first. 未找到模板,请先运行 'roll setup'。"
1032
+ exit 1
1033
+ fi
1034
+ shift
1035
+ _init_apply "$@"
1036
+ return $?
1037
+ fi
931
1038
 
932
- if [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; then
933
- python3 "${ROLL_PKG_DIR}/lib/roll-init.py" --demo
934
- if [[ "$demo" == "true" ]]; then return; fi
1039
+ if [[ "${1:-}" == -* ]]; then
1040
+ err "Unknown flag: $1 未知参数: $1"
1041
+ exit 1
935
1042
  fi
936
1043
 
937
1044
  if [[ ! -d "$ROLL_TEMPLATES" ]]; then
@@ -939,13 +1046,6 @@ cmd_init() {
939
1046
  exit 1
940
1047
  fi
941
1048
 
942
- # US-ONBOARD-009: --apply consumes onboard-plan.yaml produced by $roll-onboard
943
- if [[ "${1:-}" == "--apply" ]]; then
944
- shift
945
- _init_apply "$@"
946
- return $?
947
- fi
948
-
949
1049
  local project_dir
950
1050
  project_dir="$(pwd)"
951
1051
  local has_agents=false
@@ -953,7 +1053,6 @@ cmd_init() {
953
1053
 
954
1054
  if [[ -f "$project_dir/AGENTS.md" ]]; then
955
1055
  has_agents=true
956
- info "Re-merging global conventions... 正在重新合并全局约定..."
957
1056
  else
958
1057
  # US-ONBOARD-006: legacy project detection — guide user through $roll-onboard
959
1058
  # instead of blindly scaffolding files into an existing codebase.
@@ -963,25 +1062,90 @@ cmd_init() {
963
1062
  fi
964
1063
  fi
965
1064
 
966
- _merge_global_to_project "$project_dir"
967
- _merge_claude_to_project "$project_dir"
968
- _write_backlog "$project_dir/.roll/backlog.md"
969
- _ensure_features_dir "$project_dir/.roll/features"
970
- _write_features_md "$project_dir/.roll/features.md"
971
- print_merge_summary
1065
+ # FIX-073: Suppress per-step echoes — outcomes are captured into
1066
+ # _ROLL_MERGE_SUMMARY and rendered through the v2 UI below.
1067
+ {
1068
+ _merge_global_to_project "$project_dir"
1069
+ _merge_claude_to_project "$project_dir"
1070
+ _write_backlog "$project_dir/.roll/backlog.md"
1071
+ _ensure_features_dir "$project_dir/.roll/features"
1072
+ _write_features_md "$project_dir/.roll/features.md"
1073
+ } >/dev/null
972
1074
 
973
- echo ""
974
- info "Syncing conventions to AI tools... 正在同步约定到 AI 工具..."
975
- _sync_conventions
976
- echo ""
1075
+ local sync_status="ok"
1076
+ if ! _sync_conventions >/dev/null 2>&1; then
1077
+ sync_status="fail"
1078
+ fi
977
1079
 
978
- _install_launchd_plists "$project_dir"
1080
+ _install_launchd_plists "$project_dir" >/dev/null 2>&1 || true
979
1081
 
1082
+ _emit_init_v2_ui "$project_dir" "$has_agents" "$sync_status"
1083
+ }
1084
+
1085
+ # FIX-073: Build a real-data JSON payload from _ROLL_MERGE_SUMMARY and pipe it
1086
+ # to lib/roll-init.py for v2 UI rendering. Replaces the previous demo-only
1087
+ # render path.
1088
+ _emit_init_v2_ui() {
1089
+ local project_dir="$1"
1090
+ local has_agents="$2"
1091
+ local sync_status="${3:-ok}"
1092
+
1093
+ local header_label="INIT" subtitle="项目初始化" footer_label="Initialized"
980
1094
  if [[ "$has_agents" == "true" ]]; then
981
- ok "Done. 完成。"
982
- else
983
- ok "Initialized. 已初始化。"
1095
+ header_label="REINIT"
1096
+ subtitle="重新合并约定"
1097
+ footer_label="Re-merged"
984
1098
  fi
1099
+
1100
+ local color_flag=""
1101
+ if [[ -n "${NO_COLOR:-}" ]] || ! [ -t 1 ]; then
1102
+ color_flag="--no-color"
1103
+ fi
1104
+
1105
+ python3 - "$project_dir" "$header_label" "$subtitle" "$footer_label" "$sync_status" "${_ROLL_MERGE_SUMMARY[@]}" <<'PY' \
1106
+ | python3 "${ROLL_PKG_DIR}/lib/roll-init.py" $color_flag
1107
+ import json, sys
1108
+ project_dir, header_label, subtitle, footer_label, sync_status, *summary = sys.argv[1:]
1109
+ STATUS = {"created":"ok","merged":"ok","unchanged":"skip","overwritten":"ok","kept":"skip"}
1110
+ OP = {"created":"+","merged":"~","unchanged":"·","overwritten":"~","kept":"·"}
1111
+ by_file = {}
1112
+ for entry in summary:
1113
+ action, _sep, fname = entry.partition("|")
1114
+ if fname:
1115
+ by_file[fname] = action
1116
+
1117
+ def step(num, label, fname):
1118
+ act = by_file.get(fname)
1119
+ if not act:
1120
+ return {"num": num, "label": label, "status": "skip",
1121
+ "note": "not modified"}
1122
+ return {"num": num, "label": label, "status": STATUS.get(act, "ok"),
1123
+ "files": [[OP.get(act, "·"), fname]]}
1124
+
1125
+ steps = [
1126
+ {"num": 1, "label": "Detect project type", "status": "ok"},
1127
+ step(2, "Create AGENTS.md", "AGENTS.md"),
1128
+ step(3, "Create .roll/backlog.md", ".roll/backlog.md"),
1129
+ step(4, "Create .roll/features/", ".roll/features/"),
1130
+ step(5, "Merge existing CLAUDE.md", ".claude/CLAUDE.md"),
1131
+ {"num": 6, "label": "Link skills to AI clients", "status": sync_status},
1132
+ ]
1133
+ footer_status = "fail" if any(s["status"] == "fail" for s in steps) else "ok"
1134
+ payload = {
1135
+ "header_label": header_label,
1136
+ "subtitle": subtitle,
1137
+ "project_path": project_dir,
1138
+ "steps": steps,
1139
+ "footer": {"status": footer_status,
1140
+ "label": footer_label if footer_status == "ok" else "Init incomplete"},
1141
+ "next": [
1142
+ ["Edit .roll/backlog.md", "open the backlog and add your first US"],
1143
+ ["Run roll loop now", "execute one cycle manually to test the flow"],
1144
+ ["Enable loop scheduling", "roll loop on — let it run hourly"],
1145
+ ],
1146
+ }
1147
+ print(json.dumps(payload))
1148
+ PY
985
1149
  }
986
1150
 
987
1151
  # US-ONBOARD-006: Legacy detection
@@ -2219,9 +2383,6 @@ cmd_peer() {
2219
2383
  local context_file=""
2220
2384
  local yolo=false
2221
2385
  local subcmd=""
2222
- # US-VIEW-009: parse --demo before any side effects so the v2 renderer can
2223
- # run standalone (no peer state, no tmux session, no agent call).
2224
- local demo=false
2225
2386
 
2226
2387
  while [[ $# -gt 0 ]]; do
2227
2388
  case "$1" in
@@ -2231,7 +2392,6 @@ cmd_peer() {
2231
2392
  --tag) tag="$2"; shift 2 ;;
2232
2393
  --context) context_file="$2"; shift 2 ;;
2233
2394
  --yes|--yolo) yolo=true; shift ;;
2234
- --demo) demo=true; shift ;;
2235
2395
  status) subcmd="status"; shift ;;
2236
2396
  reset) subcmd="reset"; shift; break ;;
2237
2397
  help|--help|-h) subcmd="help"; shift ;;
@@ -2239,13 +2399,6 @@ cmd_peer() {
2239
2399
  esac
2240
2400
  done
2241
2401
 
2242
- # US-VIEW-009: ROLL_UI=v2 (default) + --demo routes to the redesigned Python view.
2243
- # Set ROLL_UI=v1 to fall back to the legacy bash implementation.
2244
- if [[ "$demo" == "true" ]] && { [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; }; then
2245
- python3 "${ROLL_PKG_DIR}/lib/roll-peer.py" --demo
2246
- return
2247
- fi
2248
-
2249
2402
  case "$subcmd" in
2250
2403
  status) cmd_peer_status; return ;;
2251
2404
  reset) cmd_peer_reset "$@"; return ;;
@@ -3787,7 +3940,7 @@ cmd_loop() {
3787
3940
  off) _loop_off ;;
3788
3941
  now) _loop_now ;;
3789
3942
  test) _loop_test ;;
3790
- status) _loop_status ;;
3943
+ status) _loop_status "$@" ;;
3791
3944
  monitor) _loop_monitor "${1:-3}" ;;
3792
3945
  runs) _loop_runs "$@" ;;
3793
3946
  events) _loop_event_log "${1:-20}" ;;
package/lib/loop-fmt.py CHANGED
@@ -116,10 +116,15 @@ class LoopFmt:
116
116
  self.pending_ci = False
117
117
  self.pending_story = False
118
118
  self.spinner = Spinner()
119
- # Track the most recent usage / model seen on assistant turns so
120
- # the result event handler can emit a 'usage' event even when
121
- # result.usage is missing.
122
- self._last_usage = None
119
+ # Accumulate token usage across all assistant turns in the cycle so
120
+ # the trailing result event can emit a 'usage' event carrying the
121
+ # cumulative totals (result.usage only carries the last turn's).
122
+ self._usage_totals = {
123
+ "input_tokens": 0,
124
+ "output_tokens": 0,
125
+ "cache_creation_tokens": 0,
126
+ "cache_read_tokens": 0,
127
+ }
123
128
  self._last_model = None
124
129
 
125
130
  def _extract_cycle_num(self, text):
@@ -161,10 +166,14 @@ class LoopFmt:
161
166
 
162
167
  def _handle_assistant(self, ev):
163
168
  msg = ev.get("message", {})
164
- # Remember the latest usage / model so the trailing result event
165
- # can emit a 'usage' event even if result.usage is empty.
166
- if msg.get("usage"):
167
- self._last_usage = msg["usage"]
169
+ # Sum token usage across turns; result.usage only carries the last
170
+ # turn so accumulating here is the only way to get cumulative totals.
171
+ u = msg.get("usage") or {}
172
+ if u:
173
+ self._usage_totals["input_tokens"] += int(u.get("input_tokens") or 0)
174
+ self._usage_totals["output_tokens"] += int(u.get("output_tokens") or 0)
175
+ self._usage_totals["cache_creation_tokens"] += int(u.get("cache_creation_input_tokens") or 0)
176
+ self._usage_totals["cache_read_tokens"] += int(u.get("cache_read_input_tokens") or 0)
168
177
  if msg.get("model"):
169
178
  self._last_model = msg["model"]
170
179
  for blk in msg.get("content", []):
@@ -341,18 +350,17 @@ class LoopFmt:
341
350
  shared = os.environ.get("LOOP_SHARED_ROOT") or os.path.expanduser("~/.shared/roll")
342
351
  if not (slug and cycle):
343
352
  return
344
- # Pull usage off the result event itself if present, otherwise off
345
- # the most recent assistant turn we observed.
346
- usage = (result_ev.get("usage") or self._last_usage or {})
353
+ # Use the cumulative totals accumulated across all assistant turns;
354
+ # result.usage is per-turn (last only) so it would under-count badly.
347
355
  model = result_ev.get("model") or self._last_model or ""
348
356
  payload = {
349
- "model": model,
350
- "input_tokens": int(usage.get("input_tokens") or 0),
351
- "output_tokens": int(usage.get("output_tokens") or 0),
352
- "cache_creation_tokens": int(usage.get("cache_creation_input_tokens") or 0),
353
- "cache_read_tokens": int(usage.get("cache_read_input_tokens") or 0),
354
- "cost_reported_usd": float(cost_usd or 0),
355
- "duration_ms": int(dur_ms or 0),
357
+ "model": model,
358
+ "input_tokens": self._usage_totals["input_tokens"],
359
+ "output_tokens": self._usage_totals["output_tokens"],
360
+ "cache_creation_tokens": self._usage_totals["cache_creation_tokens"],
361
+ "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),
356
364
  }
357
365
  evfile = os.path.join(shared, "loop", f"events-{slug}.ndjson")
358
366
  line = json.dumps({
@@ -211,51 +211,14 @@ def render(path: str) -> None:
211
211
 
212
212
  def main() -> None:
213
213
  args = sys.argv[1:]
214
- demo = "--demo" in args
215
214
  no_color = "--no-color" in args or not sys.stdout.isatty() or os.getenv("NO_COLOR")
216
215
  rr.USE_COLOR = not no_color
217
216
 
218
217
  backlog = ".roll/backlog.md"
219
- if not demo and not os.path.isfile(backlog):
218
+ if not os.path.isfile(backlog):
220
219
  print(f"Error: {backlog} not found — run 'roll init' first", file=sys.stderr)
221
220
  sys.exit(1)
222
-
223
- if demo:
224
- _write_demo(backlog)
225
- try:
226
- render(backlog)
227
- finally:
228
- os.unlink(backlog)
229
- else:
230
- render(backlog)
231
-
232
-
233
- def _write_demo(path: str) -> None:
234
- with open(path, "w") as f:
235
- f.write("""# Project Backlog
236
-
237
- ## 🐛 Bug Fixes
238
- | ID | Description | Status |
239
- |----|-------------|--------|
240
- | FIX-042 | Fix outer runner tmux kill matching wrong session | 🔨 In Progress |
241
- | FIX-043 | Handle stale state in loop now command | 📋 Todo |
242
-
243
- ## Epic: Autonomous Evolution
244
- ### Feature: autonomous-evolution
245
- | Story | Description | Status |
246
- |-------|-------------|--------|
247
- | [US-AUTO-042](.roll/features/autonomous-evolution/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
248
-
249
- ## ♻️ Refactor
250
- | ID | Description | Status |
251
- |----|-------------|--------|
252
- | REFACTOR-010 | Simplify CI test parallelism strategy | 🔒 Blocked [waiting on CI infra] |
253
-
254
- ## 💡 Ideas
255
- | ID | Description | Status |
256
- |----|-------------|--------|
257
- | IDEA-025 | Dashboard cost from list-price tokens | ⏸ Deferred [design pending] |
258
- """)
221
+ render(backlog)
259
222
 
260
223
 
261
224
  if __name__ == "__main__":
package/lib/roll-help.py CHANGED
@@ -7,7 +7,7 @@ Compact wordmark + grouped commands (AUTONOMY / PROJECT / MACHINE) + examples.
7
7
  Usage:
8
8
  python3 lib/roll-help.py # live
9
9
  python3 lib/roll-help.py --no-color
10
- python3 lib/roll-help.py --demo # same as live, no fixture needed
10
+ python3 lib/roll-help.py # static layout, no fixture needed
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -142,7 +142,6 @@ def render(version: str) -> None:
142
142
  # ════════════════════════════════════════════════════════════════════════════
143
143
  def main() -> None:
144
144
  ap = argparse.ArgumentParser(add_help=False)
145
- ap.add_argument("--demo", action="store_true")
146
145
  ap.add_argument("--no-color", dest="no_color", action="store_true")
147
146
  ap.add_argument("--en", action="store_true")
148
147
  ap.add_argument("--zh", action="store_true")
package/lib/roll-home.py CHANGED
@@ -10,7 +10,7 @@ Usage:
10
10
  python3 lib/roll-home.py # live data
11
11
  python3 lib/roll-home.py --no-color
12
12
  python3 lib/roll-home.py --en | --zh # collapse bilingual rows
13
- python3 lib/roll-home.py --demo # render with fixture data
13
+ ROLL_RENDER_FIXTURE=1 python3 lib/roll-home.py # render with fixture data (test only)
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -262,9 +262,9 @@ def _ac_completion(feature_link: str) -> Tuple[int, int]:
262
262
  return (done, total)
263
263
 
264
264
  # ════════════════════════════════════════════════════════════════════════════
265
- # Demo fixture
265
+ # Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
266
266
  # ════════════════════════════════════════════════════════════════════════════
267
- def _demo_data() -> Dict[str, Any]:
267
+ def _fixture_data() -> Dict[str, Any]:
268
268
  return dict(
269
269
  project_name="myapp", version="2026.518.3",
270
270
  agent="claude", git_branch="main", git_status="✓",
@@ -455,7 +455,6 @@ def render(d: Dict[str, Any]) -> None:
455
455
  # ════════════════════════════════════════════════════════════════════════════
456
456
  def main() -> None:
457
457
  ap = argparse.ArgumentParser(add_help=False)
458
- ap.add_argument("--demo", action="store_true")
459
458
  ap.add_argument("--no-color", dest="no_color", action="store_true")
460
459
  ap.add_argument("--en", action="store_true")
461
460
  ap.add_argument("--zh", action="store_true")
@@ -464,8 +463,8 @@ def main() -> None:
464
463
  if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
465
464
  roll_render.USE_COLOR = False
466
465
 
467
- if args.demo:
468
- d = _demo_data()
466
+ if os.environ.get("ROLL_RENDER_FIXTURE"):
467
+ d = _fixture_data()
469
468
  else:
470
469
  slug = _project_slug()
471
470
  config = _load_config()