@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 +24 -0
- package/bin/roll +232 -79
- package/lib/__pycache__/model_prices.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/loop-fmt.py +26 -18
- package/lib/roll-backlog.py +2 -39
- package/lib/roll-help.py +1 -2
- package/lib/roll-home.py +5 -6
- package/lib/roll-init.py +103 -48
- package/lib/roll-loop-status.py +33 -25
- package/lib/roll-peer.py +25 -15
- package/lib/roll-setup.py +70 -30
- package/lib/roll-status.py +4 -5
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +34 -29
- package/skills/roll-peer/SKILL.md +58 -0
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.
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
617
|
-
|
|
671
|
+
# FIX-078: launchd plist 安装从 setup 里拿掉——plist 是 per-project 资源,
|
|
672
|
+
# setup 是全局安装阶段,不应该给 cwd 留 disabled 的占位。需要时 cmd_init /
|
|
673
|
+
# _loop_on 各自会调 _install_launchd_plists。
|
|
618
674
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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 [[ "${
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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}" ;;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
-
#
|
|
120
|
-
# the result event
|
|
121
|
-
# result.usage
|
|
122
|
-
self.
|
|
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
|
-
#
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
345
|
-
#
|
|
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":
|
|
350
|
-
"input_tokens":
|
|
351
|
-
"output_tokens":
|
|
352
|
-
"cache_creation_tokens":
|
|
353
|
-
"cache_read_tokens":
|
|
354
|
-
"cost_reported_usd":
|
|
355
|
-
"duration_ms":
|
|
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({
|
package/lib/roll-backlog.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
265
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
266
266
|
# ════════════════════════════════════════════════════════════════════════════
|
|
267
|
-
def
|
|
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
|
|
468
|
-
d =
|
|
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()
|