@seanyao/roll 2026.520.1 → 2026.521.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.
- package/CHANGELOG.md +15 -0
- package/bin/roll +223 -77
- 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 +32 -24
- 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-peer/SKILL.md +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.521.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **dashboard cycle 行现在标出模型和按公开单价算的成本** — 跨账号 / 跨项目可以横向对比和加总,不再被订阅折扣藏掉真实开销 `[loop]`
|
|
8
|
+
|
|
9
|
+
### Improved
|
|
10
|
+
|
|
11
|
+
- **`roll setup` 重跑能看出"已是最新"还是"刷新了 X 项"** — 每步上报 changed / unchanged / failed,强制覆盖用 `~` 标出 `[loop]`
|
|
12
|
+
- **`roll-peer` 评审第一轮要先独立判断** — 不再被评审方预设结论带跑,跨 agent 才真的是二次判断 `[loop]`
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`roll init` 默认不再打"Project ready"假提示** — 老项目进引导分支时不再骗你说已就绪 `[legacy-onboard]`
|
|
17
|
+
|
|
3
18
|
## v2026.520.1
|
|
4
19
|
|
|
5
20
|
### 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.1"
|
|
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,154 @@ _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
|
+
_setup_snapshot() {
|
|
581
|
+
local watch="$1"
|
|
582
|
+
local -a dirs
|
|
583
|
+
IFS=':' read -r -a dirs <<<"$watch"
|
|
584
|
+
local d
|
|
585
|
+
{
|
|
586
|
+
for d in "${dirs[@]}"; do
|
|
587
|
+
[[ -d "$d" ]] || continue
|
|
588
|
+
find "$d" -type f -print0 2>/dev/null | xargs -0 cksum 2>/dev/null
|
|
589
|
+
done
|
|
590
|
+
} | sort
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
# FIX-075: run a setup step and report changed/unchanged/failed via the global
|
|
594
|
+
# _ROLL_SETUP_STATE. Caller passes the watch dir(s) plus the command + args.
|
|
595
|
+
# stdout/stderr of the inner command are suppressed (same as the previous
|
|
596
|
+
# pattern in cmd_setup) to keep the v2 UI render the only user-visible output.
|
|
597
|
+
_run_setup_step() {
|
|
598
|
+
local watch="$1"; shift
|
|
599
|
+
local before after
|
|
600
|
+
before=$(_setup_snapshot "$watch")
|
|
601
|
+
if "$@" >/dev/null 2>&1; then
|
|
602
|
+
after=$(_setup_snapshot "$watch")
|
|
603
|
+
if [[ "$before" == "$after" ]]; then
|
|
604
|
+
_ROLL_SETUP_STATE="unchanged"
|
|
605
|
+
else
|
|
606
|
+
_ROLL_SETUP_STATE="changed"
|
|
607
|
+
fi
|
|
608
|
+
else
|
|
609
|
+
_ROLL_SETUP_STATE="failed"
|
|
610
|
+
fi
|
|
611
|
+
}
|
|
612
|
+
|
|
575
613
|
cmd_setup() {
|
|
576
614
|
local force=false
|
|
577
|
-
local demo=false
|
|
578
615
|
while [[ $# -gt 0 ]]; do
|
|
579
616
|
case "$1" in
|
|
580
617
|
--force|-f) force=true; shift ;;
|
|
581
|
-
--demo) demo=true; shift ;;
|
|
582
618
|
*) err "Unknown argument: $1 未知参数: $1"; exit 1 ;;
|
|
583
619
|
esac
|
|
584
620
|
done
|
|
585
621
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
622
|
+
# Capture per-step outcomes for the v2 UI render at the end.
|
|
623
|
+
local steps_buf=()
|
|
624
|
+
_record() { steps_buf+=("$1|$2"); }
|
|
625
|
+
|
|
626
|
+
# Map snapshot-detected state to v2 UI marker. -f rewrites "changed" to
|
|
627
|
+
# "forced" so the user can tell a forced reinstall apart from a fresh
|
|
628
|
+
# install — both produce diff'd files, only -f was explicitly requested.
|
|
629
|
+
_state_to_marker() {
|
|
630
|
+
local s="$1"
|
|
631
|
+
case "$s" in
|
|
632
|
+
changed) [[ "$force" == "true" ]] && echo forced || echo ok ;;
|
|
633
|
+
unchanged) echo skip ;;
|
|
634
|
+
failed) echo fail ;;
|
|
635
|
+
*) echo fail ;;
|
|
636
|
+
esac
|
|
637
|
+
}
|
|
590
638
|
|
|
591
|
-
|
|
592
|
-
echo ""
|
|
639
|
+
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
640
|
|
|
594
|
-
_install_local "$force"
|
|
595
|
-
|
|
596
|
-
info "Syncing to AI tools... 正在同步到 AI 工具..."
|
|
597
|
-
_sync_conventions "$force"
|
|
598
|
-
echo ""
|
|
599
|
-
_sync_skills "$force"
|
|
641
|
+
_run_setup_step "$ROLL_HOME" _install_local "$force"
|
|
642
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install templates & conventions to ~/.roll"
|
|
600
643
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
_peer_ensure_state_dir
|
|
604
|
-
ok "Peer state directory ready. Peer 状态目录已就绪。"
|
|
644
|
+
_run_setup_step "$_ai_dirs" _sync_conventions "$force"
|
|
645
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Sync conventions to AI tools"
|
|
605
646
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
647
|
+
_run_setup_step "$_ai_dirs" _sync_skills "$force"
|
|
648
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install skills to ~/.claude"
|
|
649
|
+
|
|
650
|
+
_run_setup_step "$ROLL_HOME/.peer-state" _peer_ensure_state_dir
|
|
651
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Initialize peer-review state directory"
|
|
652
|
+
|
|
653
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
654
|
+
_record skip "Ensure tmux is installed (already present)"
|
|
655
|
+
else
|
|
656
|
+
if _ensure_tmux >/dev/null 2>&1 && command -v tmux >/dev/null 2>&1; then
|
|
657
|
+
_record ok "Ensure tmux is installed"
|
|
658
|
+
else
|
|
659
|
+
_record fail "Ensure tmux is installed"
|
|
660
|
+
fi
|
|
661
|
+
fi
|
|
609
662
|
|
|
610
663
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
_install_launchd_plists "$(pwd -P)"
|
|
664
|
+
_run_setup_step "$HOME/Library/LaunchAgents" _install_launchd_plists "$(pwd -P)"
|
|
665
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install launchd plists (macOS)"
|
|
614
666
|
fi
|
|
615
667
|
|
|
616
|
-
|
|
617
|
-
|
|
668
|
+
_emit_setup_v2_ui "${steps_buf[@]}"
|
|
669
|
+
}
|
|
618
670
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
671
|
+
# FIX-073: Render the cmd_setup v2 UI from per-step outcomes captured above.
|
|
672
|
+
# FIX-075: footer composition depends on how many steps actually changed —
|
|
673
|
+
# all unchanged → "no changes"; some forced (~) → "re-installed (forced)";
|
|
674
|
+
# any failed → "Setup incomplete"; otherwise → "X items refreshed".
|
|
675
|
+
_emit_setup_v2_ui() {
|
|
676
|
+
local color_flag=""
|
|
677
|
+
if [[ -n "${NO_COLOR:-}" ]] || ! [ -t 1 ]; then
|
|
678
|
+
color_flag="--no-color"
|
|
679
|
+
fi
|
|
680
|
+
|
|
681
|
+
python3 - "$@" <<'PY' \
|
|
682
|
+
| python3 "${ROLL_PKG_DIR}/lib/roll-setup.py" $color_flag
|
|
683
|
+
import json, sys
|
|
684
|
+
entries = sys.argv[1:]
|
|
685
|
+
steps = []
|
|
686
|
+
for i, entry in enumerate(entries, start=1):
|
|
687
|
+
status, _sep, label = entry.partition("|")
|
|
688
|
+
steps.append({"num": i, "label": label, "status": status})
|
|
689
|
+
|
|
690
|
+
n_failed = sum(1 for s in steps if s["status"] == "fail")
|
|
691
|
+
n_forced = sum(1 for s in steps if s["status"] == "forced")
|
|
692
|
+
n_changed = sum(1 for s in steps if s["status"] == "ok")
|
|
693
|
+
|
|
694
|
+
if n_failed:
|
|
695
|
+
footer_status = "fail"
|
|
696
|
+
label = "Setup incomplete"
|
|
697
|
+
hint = None
|
|
698
|
+
elif n_forced:
|
|
699
|
+
footer_status = "ok"
|
|
700
|
+
label = f"Setup re-installed (forced — {n_forced} item{'s' if n_forced != 1 else ''})"
|
|
701
|
+
hint = "run roll init inside a project"
|
|
702
|
+
elif n_changed == 0:
|
|
703
|
+
footer_status = "ok"
|
|
704
|
+
label = "Setup complete (no changes)"
|
|
705
|
+
hint = "everything already up to date"
|
|
706
|
+
else:
|
|
707
|
+
footer_status = "ok"
|
|
708
|
+
label = f"Setup complete ({n_changed} item{'s' if n_changed != 1 else ''} refreshed)"
|
|
709
|
+
hint = "run roll init inside a project"
|
|
710
|
+
|
|
711
|
+
payload = {
|
|
712
|
+
"header_label": "SETUP",
|
|
713
|
+
"subtitle": "初始化",
|
|
714
|
+
"steps": steps,
|
|
715
|
+
"footer": {
|
|
716
|
+
"status": footer_status,
|
|
717
|
+
"label": label,
|
|
718
|
+
"hint": hint,
|
|
719
|
+
},
|
|
720
|
+
}
|
|
721
|
+
print(json.dumps(payload))
|
|
722
|
+
PY
|
|
622
723
|
}
|
|
623
724
|
|
|
624
725
|
# ─── PR pipeline hint ────────────────────────────────────────────────────────
|
|
@@ -917,21 +1018,20 @@ _merge_claude_to_project() {
|
|
|
917
1018
|
# Existing AGENTS.md: re-merges global conventions (section-level, non-destructive)
|
|
918
1019
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
919
1020
|
cmd_init() {
|
|
920
|
-
# US-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
set -- "${args[@]:-}"
|
|
1021
|
+
# US-ONBOARD-009: --apply consumes onboard-plan.yaml produced by $roll-onboard
|
|
1022
|
+
if [[ "${1:-}" == "--apply" ]]; then
|
|
1023
|
+
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
1024
|
+
err "No templates found. Run 'roll setup' first. 未找到模板,请先运行 'roll setup'。"
|
|
1025
|
+
exit 1
|
|
1026
|
+
fi
|
|
1027
|
+
shift
|
|
1028
|
+
_init_apply "$@"
|
|
1029
|
+
return $?
|
|
1030
|
+
fi
|
|
931
1031
|
|
|
932
|
-
if [[ "${
|
|
933
|
-
|
|
934
|
-
|
|
1032
|
+
if [[ "${1:-}" == -* ]]; then
|
|
1033
|
+
err "Unknown flag: $1 未知参数: $1"
|
|
1034
|
+
exit 1
|
|
935
1035
|
fi
|
|
936
1036
|
|
|
937
1037
|
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
@@ -939,13 +1039,6 @@ cmd_init() {
|
|
|
939
1039
|
exit 1
|
|
940
1040
|
fi
|
|
941
1041
|
|
|
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
1042
|
local project_dir
|
|
950
1043
|
project_dir="$(pwd)"
|
|
951
1044
|
local has_agents=false
|
|
@@ -953,7 +1046,6 @@ cmd_init() {
|
|
|
953
1046
|
|
|
954
1047
|
if [[ -f "$project_dir/AGENTS.md" ]]; then
|
|
955
1048
|
has_agents=true
|
|
956
|
-
info "Re-merging global conventions... 正在重新合并全局约定..."
|
|
957
1049
|
else
|
|
958
1050
|
# US-ONBOARD-006: legacy project detection — guide user through $roll-onboard
|
|
959
1051
|
# instead of blindly scaffolding files into an existing codebase.
|
|
@@ -963,25 +1055,90 @@ cmd_init() {
|
|
|
963
1055
|
fi
|
|
964
1056
|
fi
|
|
965
1057
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1058
|
+
# FIX-073: Suppress per-step echoes — outcomes are captured into
|
|
1059
|
+
# _ROLL_MERGE_SUMMARY and rendered through the v2 UI below.
|
|
1060
|
+
{
|
|
1061
|
+
_merge_global_to_project "$project_dir"
|
|
1062
|
+
_merge_claude_to_project "$project_dir"
|
|
1063
|
+
_write_backlog "$project_dir/.roll/backlog.md"
|
|
1064
|
+
_ensure_features_dir "$project_dir/.roll/features"
|
|
1065
|
+
_write_features_md "$project_dir/.roll/features.md"
|
|
1066
|
+
} >/dev/null
|
|
972
1067
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1068
|
+
local sync_status="ok"
|
|
1069
|
+
if ! _sync_conventions >/dev/null 2>&1; then
|
|
1070
|
+
sync_status="fail"
|
|
1071
|
+
fi
|
|
977
1072
|
|
|
978
|
-
_install_launchd_plists "$project_dir"
|
|
1073
|
+
_install_launchd_plists "$project_dir" >/dev/null 2>&1 || true
|
|
979
1074
|
|
|
1075
|
+
_emit_init_v2_ui "$project_dir" "$has_agents" "$sync_status"
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
# FIX-073: Build a real-data JSON payload from _ROLL_MERGE_SUMMARY and pipe it
|
|
1079
|
+
# to lib/roll-init.py for v2 UI rendering. Replaces the previous demo-only
|
|
1080
|
+
# render path.
|
|
1081
|
+
_emit_init_v2_ui() {
|
|
1082
|
+
local project_dir="$1"
|
|
1083
|
+
local has_agents="$2"
|
|
1084
|
+
local sync_status="${3:-ok}"
|
|
1085
|
+
|
|
1086
|
+
local header_label="INIT" subtitle="项目初始化" footer_label="Initialized"
|
|
980
1087
|
if [[ "$has_agents" == "true" ]]; then
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1088
|
+
header_label="REINIT"
|
|
1089
|
+
subtitle="重新合并约定"
|
|
1090
|
+
footer_label="Re-merged"
|
|
984
1091
|
fi
|
|
1092
|
+
|
|
1093
|
+
local color_flag=""
|
|
1094
|
+
if [[ -n "${NO_COLOR:-}" ]] || ! [ -t 1 ]; then
|
|
1095
|
+
color_flag="--no-color"
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
1098
|
+
python3 - "$project_dir" "$header_label" "$subtitle" "$footer_label" "$sync_status" "${_ROLL_MERGE_SUMMARY[@]}" <<'PY' \
|
|
1099
|
+
| python3 "${ROLL_PKG_DIR}/lib/roll-init.py" $color_flag
|
|
1100
|
+
import json, sys
|
|
1101
|
+
project_dir, header_label, subtitle, footer_label, sync_status, *summary = sys.argv[1:]
|
|
1102
|
+
STATUS = {"created":"ok","merged":"ok","unchanged":"skip","overwritten":"ok","kept":"skip"}
|
|
1103
|
+
OP = {"created":"+","merged":"~","unchanged":"·","overwritten":"~","kept":"·"}
|
|
1104
|
+
by_file = {}
|
|
1105
|
+
for entry in summary:
|
|
1106
|
+
action, _sep, fname = entry.partition("|")
|
|
1107
|
+
if fname:
|
|
1108
|
+
by_file[fname] = action
|
|
1109
|
+
|
|
1110
|
+
def step(num, label, fname):
|
|
1111
|
+
act = by_file.get(fname)
|
|
1112
|
+
if not act:
|
|
1113
|
+
return {"num": num, "label": label, "status": "skip",
|
|
1114
|
+
"note": "not modified"}
|
|
1115
|
+
return {"num": num, "label": label, "status": STATUS.get(act, "ok"),
|
|
1116
|
+
"files": [[OP.get(act, "·"), fname]]}
|
|
1117
|
+
|
|
1118
|
+
steps = [
|
|
1119
|
+
{"num": 1, "label": "Detect project type", "status": "ok"},
|
|
1120
|
+
step(2, "Create AGENTS.md", "AGENTS.md"),
|
|
1121
|
+
step(3, "Create .roll/backlog.md", ".roll/backlog.md"),
|
|
1122
|
+
step(4, "Create .roll/features/", ".roll/features/"),
|
|
1123
|
+
step(5, "Merge existing CLAUDE.md", ".claude/CLAUDE.md"),
|
|
1124
|
+
{"num": 6, "label": "Link skills to AI clients", "status": sync_status},
|
|
1125
|
+
]
|
|
1126
|
+
footer_status = "fail" if any(s["status"] == "fail" for s in steps) else "ok"
|
|
1127
|
+
payload = {
|
|
1128
|
+
"header_label": header_label,
|
|
1129
|
+
"subtitle": subtitle,
|
|
1130
|
+
"project_path": project_dir,
|
|
1131
|
+
"steps": steps,
|
|
1132
|
+
"footer": {"status": footer_status,
|
|
1133
|
+
"label": footer_label if footer_status == "ok" else "Init incomplete"},
|
|
1134
|
+
"next": [
|
|
1135
|
+
["Edit .roll/backlog.md", "open the backlog and add your first US"],
|
|
1136
|
+
["Run roll loop now", "execute one cycle manually to test the flow"],
|
|
1137
|
+
["Enable loop scheduling", "roll loop on — let it run hourly"],
|
|
1138
|
+
],
|
|
1139
|
+
}
|
|
1140
|
+
print(json.dumps(payload))
|
|
1141
|
+
PY
|
|
985
1142
|
}
|
|
986
1143
|
|
|
987
1144
|
# US-ONBOARD-006: Legacy detection
|
|
@@ -2219,9 +2376,6 @@ cmd_peer() {
|
|
|
2219
2376
|
local context_file=""
|
|
2220
2377
|
local yolo=false
|
|
2221
2378
|
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
2379
|
|
|
2226
2380
|
while [[ $# -gt 0 ]]; do
|
|
2227
2381
|
case "$1" in
|
|
@@ -2231,7 +2385,6 @@ cmd_peer() {
|
|
|
2231
2385
|
--tag) tag="$2"; shift 2 ;;
|
|
2232
2386
|
--context) context_file="$2"; shift 2 ;;
|
|
2233
2387
|
--yes|--yolo) yolo=true; shift ;;
|
|
2234
|
-
--demo) demo=true; shift ;;
|
|
2235
2388
|
status) subcmd="status"; shift ;;
|
|
2236
2389
|
reset) subcmd="reset"; shift; break ;;
|
|
2237
2390
|
help|--help|-h) subcmd="help"; shift ;;
|
|
@@ -2239,13 +2392,6 @@ cmd_peer() {
|
|
|
2239
2392
|
esac
|
|
2240
2393
|
done
|
|
2241
2394
|
|
|
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
2395
|
case "$subcmd" in
|
|
2250
2396
|
status) cmd_peer_status; return ;;
|
|
2251
2397
|
reset) cmd_peer_reset "$@"; return ;;
|
|
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()
|
package/lib/roll-init.py
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""roll-init — v2 terminal view for `roll init
|
|
2
|
+
"""roll-init — v2 terminal view for `roll init`.
|
|
3
|
+
|
|
4
|
+
Reads a single JSON document from stdin describing real step outcomes
|
|
5
|
+
captured by the bash flow in `bin/roll cmd_init`. Renders the v2 UI
|
|
6
|
+
(banner + horizontal rule + numbered steps + NEXT block) preserving the
|
|
7
|
+
visual style of US-VIEW-008 but reflecting what actually happened.
|
|
8
|
+
|
|
9
|
+
Input schema (single JSON object on stdin):
|
|
10
|
+
{
|
|
11
|
+
"kind": "init", # informational; UI label uses header_label
|
|
12
|
+
"header_label": "INIT", # banner label (e.g. "INIT" / "REINIT")
|
|
13
|
+
"subtitle": "项目初始化", # banner subtitle
|
|
14
|
+
"project_path": "/path/to/project", # right-aligned banner text
|
|
15
|
+
"steps": [
|
|
16
|
+
{"num": 1, "label": "Detect project type", "status": "ok"},
|
|
17
|
+
{"num": 2, "label": "Create AGENTS.md", "status": "ok",
|
|
18
|
+
"files": [["+", "AGENTS.md"]]},
|
|
19
|
+
{"num": 3, "label": "...", "status": "skip", "note": "already exists"},
|
|
20
|
+
{"num": 4, "label": "...", "status": "fail", "error": "permission denied"}
|
|
21
|
+
],
|
|
22
|
+
"footer": {"status": "ok", "label": "Project ready"},
|
|
23
|
+
"next": [["Edit .roll/backlog.md", "open the backlog and add your first US"]]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
`status` values: ok | skip | fail.
|
|
27
|
+
`files` ops: "+" created "~" merged "·" unchanged "✗" failed.
|
|
28
|
+
|
|
29
|
+
If stdin is empty or invalid JSON, exit 1 with a short message — the
|
|
30
|
+
renderer is no longer runnable standalone, it always renders real data.
|
|
31
|
+
"""
|
|
3
32
|
from __future__ import annotations
|
|
4
33
|
|
|
5
34
|
import argparse
|
|
35
|
+
import json
|
|
6
36
|
import os
|
|
7
37
|
import sys
|
|
8
38
|
|
|
@@ -12,32 +42,6 @@ if _LIB_DIR not in sys.path:
|
|
|
12
42
|
import roll_render
|
|
13
43
|
from roll_render import c, row, COLS
|
|
14
44
|
|
|
15
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# Demo data — 6 steps mirror cmd_init's actual phases.
|
|
17
|
-
# AC text: detect → AGENTS.md → BACKLOG.md → docs/features/ → merge CLAUDE.md → link skills
|
|
18
|
-
# Each entry: (label, [(op, filename)]) where op ∈ {"+", "~"}.
|
|
19
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
20
|
-
|
|
21
|
-
_DEMO_STEPS = [
|
|
22
|
-
("Detect project type", []),
|
|
23
|
-
("Create AGENTS.md", [("+", "AGENTS.md")]),
|
|
24
|
-
("Create .roll/backlog.md", [("+", ".roll/backlog.md")]),
|
|
25
|
-
("Create .roll/features/", [("+", ".roll/features/")]),
|
|
26
|
-
("Merge existing CLAUDE.md", [("~", "CLAUDE.md")]),
|
|
27
|
-
("Link skills to AI clients", [("+", "~/.claude/skills/roll-build"),
|
|
28
|
-
("+", "~/.claude/skills/roll-fix")]),
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
_NEXT_STEPS = [
|
|
32
|
-
("Edit .roll/backlog.md", "open the backlog and add your first US"),
|
|
33
|
-
("Run roll loop now", "execute one cycle manually to test the flow"),
|
|
34
|
-
("Enable loop scheduling", "roll loop on — let it run hourly"),
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
39
|
-
# Render
|
|
40
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
41
45
|
|
|
42
46
|
def _divider(char: str = "─") -> None:
|
|
43
47
|
print(c("dim", char * min(COLS, 80)))
|
|
@@ -48,53 +52,104 @@ def _op_marker(op: str) -> str:
|
|
|
48
52
|
return c("green", "+", bold=True)
|
|
49
53
|
if op == "~":
|
|
50
54
|
return c("amber", "~", bold=True)
|
|
55
|
+
if op == "·":
|
|
56
|
+
return c("dim", "·")
|
|
57
|
+
if op == "✗":
|
|
58
|
+
return c("red", "✗", bold=True)
|
|
51
59
|
return c("dim", op)
|
|
52
60
|
|
|
53
61
|
|
|
54
|
-
def
|
|
55
|
-
|
|
62
|
+
def _step_icon(status: str) -> str:
|
|
63
|
+
if status == "ok":
|
|
64
|
+
return c("green", "✓")
|
|
65
|
+
if status == "skip":
|
|
66
|
+
return c("amber", "↷")
|
|
67
|
+
if status == "fail":
|
|
68
|
+
return c("red", "✗", bold=True)
|
|
69
|
+
return c("dim", "·")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _file_color(op: str) -> str:
|
|
73
|
+
if op == "+":
|
|
74
|
+
return "green"
|
|
75
|
+
if op == "~":
|
|
76
|
+
return "amber"
|
|
77
|
+
if op == "✗":
|
|
78
|
+
return "red"
|
|
79
|
+
return "dim"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render(payload: dict) -> None:
|
|
83
|
+
header_label = payload.get("header_label", "INIT")
|
|
84
|
+
subtitle = payload.get("subtitle", "项目初始化")
|
|
85
|
+
project_path = payload.get("project_path", "")
|
|
86
|
+
|
|
87
|
+
left = " " + c("blue", header_label, bold=True) + c("dim", " · ") + c("dim", subtitle)
|
|
56
88
|
right = c("dim", project_path) + " "
|
|
57
89
|
print(row(left, right))
|
|
58
90
|
_divider()
|
|
59
91
|
print()
|
|
60
92
|
|
|
61
|
-
for
|
|
62
|
-
num
|
|
63
|
-
icon =
|
|
93
|
+
for step in payload.get("steps", []):
|
|
94
|
+
num = c("dim", f" {step.get('num', 0)}.")
|
|
95
|
+
icon = _step_icon(step.get("status", "ok"))
|
|
96
|
+
label = step.get("label", "")
|
|
64
97
|
print(f"{num} {icon} {label}")
|
|
65
|
-
for
|
|
98
|
+
for entry in step.get("files", []) or []:
|
|
99
|
+
op, fname = entry[0], entry[1]
|
|
66
100
|
mark = _op_marker(op)
|
|
67
|
-
color =
|
|
101
|
+
color = _file_color(op)
|
|
68
102
|
print(" " + mark + " " + c(color, fname))
|
|
103
|
+
note = step.get("note") or step.get("error")
|
|
104
|
+
if note:
|
|
105
|
+
tone = "red" if step.get("status") == "fail" else "dim"
|
|
106
|
+
print(" " + c(tone, str(note)))
|
|
69
107
|
|
|
70
108
|
print()
|
|
71
109
|
_divider()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
|
|
111
|
+
footer = payload.get("footer") or {}
|
|
112
|
+
f_status = footer.get("status", "ok")
|
|
113
|
+
f_label = footer.get("label", "Done")
|
|
114
|
+
icon_color = "green" if f_status == "ok" else "red"
|
|
115
|
+
icon = "✓" if f_status == "ok" else "✗"
|
|
116
|
+
print(" " + c(icon_color, icon) + " " + c(icon_color, f_label, bold=True))
|
|
117
|
+
|
|
118
|
+
next_items = payload.get("next") or []
|
|
119
|
+
if next_items:
|
|
120
|
+
print()
|
|
121
|
+
print(" " + c("pink", "NEXT", bold=True) + c("dim", " · 下一步"))
|
|
122
|
+
for i, entry in enumerate(next_items, start=1):
|
|
123
|
+
label = entry[0]
|
|
124
|
+
hint = entry[1] if len(entry) > 1 else ""
|
|
125
|
+
num = c("dim", f" {i}.")
|
|
126
|
+
print(f"{num} {c('fg', label, bold=True)}")
|
|
127
|
+
if hint:
|
|
128
|
+
print(" " + c("dim", hint))
|
|
79
129
|
_divider("═")
|
|
80
130
|
|
|
81
131
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
def _read_payload() -> dict:
|
|
133
|
+
raw = sys.stdin.read()
|
|
134
|
+
if not raw.strip():
|
|
135
|
+
sys.stderr.write("roll-init.py: expected JSON on stdin\n")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
try:
|
|
138
|
+
return json.loads(raw)
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
sys.stderr.write(f"roll-init.py: invalid JSON on stdin: {exc}\n")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
85
143
|
|
|
86
144
|
def main() -> None:
|
|
87
145
|
ap = argparse.ArgumentParser(add_help=False)
|
|
88
|
-
ap.add_argument("--demo", action="store_true")
|
|
89
146
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
90
|
-
ap.add_argument("--en", action="store_true")
|
|
91
|
-
ap.add_argument("--zh", action="store_true")
|
|
92
147
|
args, _ = ap.parse_known_args()
|
|
93
148
|
|
|
94
149
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
95
150
|
roll_render.USE_COLOR = False
|
|
96
151
|
|
|
97
|
-
|
|
152
|
+
render(_read_payload())
|
|
98
153
|
|
|
99
154
|
|
|
100
155
|
if __name__ == "__main__":
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -17,7 +17,7 @@ Usage:
|
|
|
17
17
|
python3 lib/roll-loop-status.py --days 7
|
|
18
18
|
python3 lib/roll-loop-status.py --no-color
|
|
19
19
|
python3 lib/roll-loop-status.py --en | --zh # collapse bilingual rows
|
|
20
|
-
python3 lib/roll-loop-status.py
|
|
20
|
+
ROLL_RENDER_FIXTURE=1 python3 lib/roll-loop-status.py # render with fixture data (test only)
|
|
21
21
|
|
|
22
22
|
Wire it in bin/roll under `loop status` (replace _loop_status with a call to
|
|
23
23
|
this script).
|
|
@@ -206,9 +206,19 @@ def aggregate(events: List[Dict[str, Any]], cron: List[Dict[str, Any]]) -> List[
|
|
|
206
206
|
elif stage == "usage":
|
|
207
207
|
# US-LOOP-004: loop-fmt emits this with full token / cost data.
|
|
208
208
|
# Detail is a dict (not the legacy string form).
|
|
209
|
+
# US-VIEW-010: token counts are per-turn deltas — sum across events
|
|
210
|
+
# so list-price cost computed from totals matches actual API usage.
|
|
211
|
+
# Non-additive fields (model, cost_reported_usd, duration_ms) take
|
|
212
|
+
# the last value seen.
|
|
209
213
|
d = e.get("detail") or {}
|
|
210
214
|
if isinstance(d, dict):
|
|
211
|
-
cy
|
|
215
|
+
prev = cy.get("usage_event") or {}
|
|
216
|
+
merged = dict(prev)
|
|
217
|
+
merged.update(d)
|
|
218
|
+
for k in ("input_tokens", "output_tokens",
|
|
219
|
+
"cache_creation_tokens", "cache_read_tokens"):
|
|
220
|
+
merged[k] = int(prev.get(k) or 0) + int(d.get(k) or 0)
|
|
221
|
+
cy["usage_event"] = merged
|
|
212
222
|
elif stage in ("test", "build") and e.get("outcome") == "fail":
|
|
213
223
|
cy["fail_detail"] = detail or stage
|
|
214
224
|
|
|
@@ -315,8 +325,7 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
315
325
|
for cy in cycles:
|
|
316
326
|
# Path 1: usage event written by loop-fmt at result time.
|
|
317
327
|
ue = cy.get("usage_event")
|
|
318
|
-
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")
|
|
319
|
-
or ue.get("cost_reported_usd")):
|
|
328
|
+
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")):
|
|
320
329
|
cy["tokens"] = mp.total_tokens(
|
|
321
330
|
input_tokens=ue.get("input_tokens", 0),
|
|
322
331
|
output_tokens=ue.get("output_tokens", 0),
|
|
@@ -324,20 +333,18 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
324
333
|
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
325
334
|
)
|
|
326
335
|
cy["model"] = ue.get("model")
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
340
|
-
)
|
|
336
|
+
# US-VIEW-010: aggregate now sums per-turn usage tokens, so the
|
|
337
|
+
# totals in `ue` reflect the whole cycle. Always compute cost at
|
|
338
|
+
# list price for cross-account comparability — supersedes FIX-060
|
|
339
|
+
# which preferred cost_reported_usd as a workaround for
|
|
340
|
+
# last-event-only token counts (that root cause is now gone).
|
|
341
|
+
cy["cost_list"] = mp.compute_list_cost(
|
|
342
|
+
ue.get("model"),
|
|
343
|
+
input_tokens=ue.get("input_tokens", 0),
|
|
344
|
+
output_tokens=ue.get("output_tokens", 0),
|
|
345
|
+
cache_creation_tokens=ue.get("cache_creation_tokens", 0),
|
|
346
|
+
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
347
|
+
)
|
|
341
348
|
if ue.get("duration_ms") and not cy.get("duration_s"):
|
|
342
349
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
343
350
|
continue
|
|
@@ -733,9 +740,9 @@ def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
|
|
|
733
740
|
return nxt.strftime("%H:%M") + f" · in {mins}m {secs:02d}s"
|
|
734
741
|
|
|
735
742
|
# ════════════════════════════════════════════════════════════════════════════
|
|
736
|
-
#
|
|
743
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
737
744
|
# ════════════════════════════════════════════════════════════════════════════
|
|
738
|
-
def
|
|
745
|
+
def _fixture_data():
|
|
739
746
|
now = datetime.now(timezone.utc)
|
|
740
747
|
events, cron = [], []
|
|
741
748
|
cycle_id = 0
|
|
@@ -788,7 +795,6 @@ def main(argv=None):
|
|
|
788
795
|
p.add_argument("--no-color", action="store_true", help="strip ANSI (also honors NO_COLOR=1)")
|
|
789
796
|
p.add_argument("--en", action="store_true", help="EN rows only")
|
|
790
797
|
p.add_argument("--zh", action="store_true", help="ZH rows only")
|
|
791
|
-
p.add_argument("--demo", action="store_true", help="render with fixture data")
|
|
792
798
|
args = p.parse_args(argv)
|
|
793
799
|
|
|
794
800
|
roll_render.USE_COLOR = (not args.no_color
|
|
@@ -797,10 +803,12 @@ def main(argv=None):
|
|
|
797
803
|
|
|
798
804
|
lang = "en" if args.en else ("zh" if args.zh else "both")
|
|
799
805
|
|
|
800
|
-
|
|
801
|
-
|
|
806
|
+
use_fixture = bool(os.environ.get("ROLL_RENDER_FIXTURE"))
|
|
807
|
+
if use_fixture:
|
|
808
|
+
events, cron, state, backlog = _fixture_data()
|
|
802
809
|
runs = {}
|
|
803
810
|
git_merges = {}
|
|
811
|
+
slug = None
|
|
804
812
|
else:
|
|
805
813
|
slug = project_slug()
|
|
806
814
|
events = load_events(slug, args.days)
|
|
@@ -812,7 +820,7 @@ def main(argv=None):
|
|
|
812
820
|
|
|
813
821
|
render(events, cron, state, backlog, days=args.days, lang=lang,
|
|
814
822
|
runs=runs, git_merges=git_merges,
|
|
815
|
-
claude_slug=
|
|
823
|
+
claude_slug=slug)
|
|
816
824
|
|
|
817
825
|
if __name__ == "__main__":
|
|
818
826
|
try:
|
package/lib/roll-peer.py
CHANGED
|
@@ -50,10 +50,11 @@ def _agent_c(name: str) -> str:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
# ════════════════════════════════════════════════════════════════════════════
|
|
53
|
-
#
|
|
53
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
54
|
+
# Illustrative cross-agent review: claude proposes, codex reviews
|
|
54
55
|
# ════════════════════════════════════════════════════════════════════════════
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
_FIXTURE_SUBJECT = {
|
|
57
58
|
"story": "US-AUTH-014",
|
|
58
59
|
"title": "Session refresh fallback when refresh-token API 5xx",
|
|
59
60
|
"pr": "#412",
|
|
@@ -63,7 +64,7 @@ _DEMO_SUBJECT = {
|
|
|
63
64
|
"reviewer": "codex",
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
_FIXTURE_ROUNDS = [
|
|
67
68
|
{
|
|
68
69
|
"n": 1,
|
|
69
70
|
"hint": "first pass — proposer ships, reviewer probes",
|
|
@@ -93,13 +94,13 @@ _DEMO_ROUNDS = [
|
|
|
93
94
|
},
|
|
94
95
|
]
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
_FIXTURE_VERDICT = {
|
|
97
98
|
"outcome": "approved",
|
|
98
99
|
"reason": "2 rounds · 5 turns · all blocks resolved",
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
_FIXTURE_ARTIFACT = "~/.roll/.peer-state/logs/20260519_213700_claude_codex.md"
|
|
103
|
+
_FIXTURE_NEXT = [
|
|
103
104
|
("Continue execution", "claude resumes work on US-AUTH-014"),
|
|
104
105
|
("Inspect log", "open the artifact above to replay the transcript"),
|
|
105
106
|
]
|
|
@@ -205,19 +206,19 @@ def _footer(artifact: str, next_steps: list) -> None:
|
|
|
205
206
|
# Top-level render
|
|
206
207
|
# ════════════════════════════════════════════════════════════════════════════
|
|
207
208
|
|
|
208
|
-
def
|
|
209
|
-
_eyebrow(
|
|
209
|
+
def render_fixture() -> None:
|
|
210
|
+
_eyebrow(_FIXTURE_SUBJECT["trigger"])
|
|
210
211
|
_divider()
|
|
211
212
|
print()
|
|
212
|
-
_subject(
|
|
213
|
+
_subject(_FIXTURE_SUBJECT)
|
|
213
214
|
print()
|
|
214
|
-
_pair_overview(
|
|
215
|
-
for rd in
|
|
215
|
+
_pair_overview(_FIXTURE_SUBJECT)
|
|
216
|
+
for rd in _FIXTURE_ROUNDS:
|
|
216
217
|
_round_header(rd["n"], rd["hint"])
|
|
217
218
|
for agent, weight, body in rd["turns"]:
|
|
218
219
|
_turn(agent, weight, body)
|
|
219
|
-
_verdict(
|
|
220
|
-
_footer(
|
|
220
|
+
_verdict(_FIXTURE_VERDICT)
|
|
221
|
+
_footer(_FIXTURE_ARTIFACT, _FIXTURE_NEXT)
|
|
221
222
|
|
|
222
223
|
|
|
223
224
|
# ════════════════════════════════════════════════════════════════════════════
|
|
@@ -226,7 +227,6 @@ def render_demo() -> None:
|
|
|
226
227
|
|
|
227
228
|
def main() -> None:
|
|
228
229
|
ap = argparse.ArgumentParser(add_help=False)
|
|
229
|
-
ap.add_argument("--demo", action="store_true")
|
|
230
230
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
231
231
|
ap.add_argument("--en", action="store_true")
|
|
232
232
|
ap.add_argument("--zh", action="store_true")
|
|
@@ -235,7 +235,17 @@ def main() -> None:
|
|
|
235
235
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
236
236
|
roll_render.USE_COLOR = False
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
# FIX-076: this standalone entrypoint only knows how to render the fixture
|
|
239
|
+
# transcript (for UI tests). Real peer review is orchestrated by bin/roll
|
|
240
|
+
# and never invokes this main(). Require an explicit opt-in so a stray
|
|
241
|
+
# `python3 lib/roll-peer.py` invocation can't masquerade as live output.
|
|
242
|
+
if not os.environ.get("ROLL_RENDER_FIXTURE"):
|
|
243
|
+
print("Error: lib/roll-peer.py only renders fixture data; "
|
|
244
|
+
"set ROLL_RENDER_FIXTURE=1 to use it (test-only).",
|
|
245
|
+
file=sys.stderr)
|
|
246
|
+
sys.exit(2)
|
|
247
|
+
|
|
248
|
+
render_fixture()
|
|
239
249
|
|
|
240
250
|
|
|
241
251
|
if __name__ == "__main__":
|
package/lib/roll-setup.py
CHANGED
|
@@ -1,61 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""roll-setup — v2 terminal view for `roll setup`.
|
|
2
|
+
"""roll-setup — v2 terminal view for `roll setup`.
|
|
3
|
+
|
|
4
|
+
Reads a single JSON document from stdin describing the real outcomes of
|
|
5
|
+
`bin/roll cmd_setup`'s step sequence (detect platform / install skills /
|
|
6
|
+
sync conventions / etc). Renders the v2 UI preserving the visual style
|
|
7
|
+
of US-VIEW-007 while reflecting actual results.
|
|
8
|
+
|
|
9
|
+
Input schema matches `lib/roll-init.py` (see that file for details).
|
|
10
|
+
"""
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
5
15
|
import os
|
|
6
16
|
import sys
|
|
7
17
|
|
|
8
18
|
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
9
19
|
if _LIB_DIR not in sys.path:
|
|
10
20
|
sys.path.insert(0, _LIB_DIR)
|
|
21
|
+
import roll_render
|
|
11
22
|
from roll_render import c, row, COLS
|
|
12
23
|
|
|
13
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
14
|
-
# Demo data
|
|
15
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
17
|
-
_DEMO_STEPS = [
|
|
18
|
-
"Detect platform & shell",
|
|
19
|
-
"Fetch latest roll version",
|
|
20
|
-
"Install skills to ~/.claude",
|
|
21
|
-
"Symlink bin/roll to PATH",
|
|
22
|
-
"Check for config drift",
|
|
23
|
-
"Apply convention templates",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
-
# Rendering
|
|
28
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
29
24
|
|
|
30
25
|
def _divider(char: str = "─") -> None:
|
|
31
26
|
print(c("dim", char * min(COLS, 80)))
|
|
32
27
|
|
|
33
28
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
def _step_icon(status: str) -> str:
|
|
30
|
+
if status == "ok":
|
|
31
|
+
return c("green", "✓")
|
|
32
|
+
if status == "skip":
|
|
33
|
+
return c("amber", "↷")
|
|
34
|
+
if status == "forced":
|
|
35
|
+
return c("blue", "~")
|
|
36
|
+
if status == "fail":
|
|
37
|
+
return c("red", "✗", bold=True)
|
|
38
|
+
return c("dim", "·")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render(payload: dict) -> None:
|
|
42
|
+
header_label = payload.get("header_label", "SETUP")
|
|
43
|
+
subtitle = payload.get("subtitle", "初始化")
|
|
44
|
+
right_text = payload.get("project_path") or payload.get("right", "")
|
|
45
|
+
|
|
46
|
+
left = " " + c("blue", header_label, bold=True) + c("dim", " · ") + c("dim", subtitle)
|
|
47
|
+
right = c("dim", right_text) + " " if right_text else ""
|
|
48
|
+
print(row(left, right))
|
|
38
49
|
_divider()
|
|
39
50
|
print()
|
|
40
51
|
|
|
41
|
-
for
|
|
42
|
-
num
|
|
43
|
-
icon =
|
|
52
|
+
for step in payload.get("steps", []):
|
|
53
|
+
num = c("dim", f" {step.get('num', 0)}.")
|
|
54
|
+
icon = _step_icon(step.get("status", "ok"))
|
|
55
|
+
label = step.get("label", "")
|
|
44
56
|
print(f"{num} {icon} {label}")
|
|
57
|
+
note = step.get("note") or step.get("error")
|
|
58
|
+
if note:
|
|
59
|
+
tone = "red" if step.get("status") == "fail" else "dim"
|
|
60
|
+
print(" " + c(tone, str(note)))
|
|
45
61
|
|
|
46
62
|
print()
|
|
47
63
|
_divider()
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
|
|
65
|
+
footer = payload.get("footer") or {}
|
|
66
|
+
f_status = footer.get("status", "ok")
|
|
67
|
+
f_label = footer.get("label", "Setup complete")
|
|
68
|
+
icon_color = "green" if f_status == "ok" else "red"
|
|
69
|
+
msg = c(icon_color, f_label)
|
|
70
|
+
next_hint = footer.get("hint")
|
|
71
|
+
if next_hint:
|
|
72
|
+
print(f" {msg} — {next_hint}")
|
|
73
|
+
else:
|
|
74
|
+
print(f" {msg}")
|
|
50
75
|
_divider("═")
|
|
51
76
|
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
def _read_payload() -> dict:
|
|
79
|
+
raw = sys.stdin.read()
|
|
80
|
+
if not raw.strip():
|
|
81
|
+
sys.stderr.write("roll-setup.py: expected JSON on stdin\n")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
try:
|
|
84
|
+
return json.loads(raw)
|
|
85
|
+
except json.JSONDecodeError as exc:
|
|
86
|
+
sys.stderr.write(f"roll-setup.py: invalid JSON on stdin: {exc}\n")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
56
89
|
|
|
57
90
|
def main() -> None:
|
|
58
|
-
|
|
91
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
92
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
93
|
+
args, _ = ap.parse_known_args()
|
|
94
|
+
|
|
95
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
96
|
+
roll_render.USE_COLOR = False
|
|
97
|
+
|
|
98
|
+
render(_read_payload())
|
|
59
99
|
|
|
60
100
|
|
|
61
101
|
if __name__ == "__main__":
|
package/lib/roll-status.py
CHANGED
|
@@ -8,7 +8,7 @@ project templates, and this-project metrics.
|
|
|
8
8
|
Usage:
|
|
9
9
|
python3 lib/roll-status.py # live data
|
|
10
10
|
python3 lib/roll-status.py --no-color
|
|
11
|
-
python3 lib/roll-status.py
|
|
11
|
+
ROLL_RENDER_FIXTURE=1 python3 lib/roll-status.py # render with fixture data (test only)
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -149,9 +149,9 @@ def _launchd_state(service: str, slug: str) -> str:
|
|
|
149
149
|
return "installed-off"
|
|
150
150
|
|
|
151
151
|
# ════════════════════════════════════════════════════════════════════════════
|
|
152
|
-
#
|
|
152
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
153
153
|
# ════════════════════════════════════════════════════════════════════════════
|
|
154
|
-
def
|
|
154
|
+
def _fixture_data() -> Dict[str, Any]:
|
|
155
155
|
return dict(
|
|
156
156
|
conventions=[
|
|
157
157
|
("AGENTS.md", True), ("CLAUDE.md", True), ("GEMINI.md", False),
|
|
@@ -347,7 +347,6 @@ def _live_data() -> Dict[str, Any]:
|
|
|
347
347
|
# ════════════════════════════════════════════════════════════════════════════
|
|
348
348
|
def main() -> None:
|
|
349
349
|
ap = argparse.ArgumentParser(add_help=False)
|
|
350
|
-
ap.add_argument("--demo", action="store_true")
|
|
351
350
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
352
351
|
ap.add_argument("--en", action="store_true")
|
|
353
352
|
ap.add_argument("--zh", action="store_true")
|
|
@@ -356,7 +355,7 @@ def main() -> None:
|
|
|
356
355
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
357
356
|
roll_render.USE_COLOR = False
|
|
358
357
|
|
|
359
|
-
d =
|
|
358
|
+
d = _fixture_data() if os.environ.get("ROLL_RENDER_FIXTURE") else _live_data()
|
|
360
359
|
|
|
361
360
|
_render_health(d)
|
|
362
361
|
_render_global_conventions(d["conventions"])
|
package/package.json
CHANGED
|
@@ -105,11 +105,67 @@ Rules:
|
|
|
105
105
|
- Do not copy README text. List file pointers only.
|
|
106
106
|
- Never include secrets, tokens, credentials, or `.env` content.
|
|
107
107
|
- Even if logically a continuation, treat as round=1 if the peer has **no prior context**.
|
|
108
|
+
- **Do NOT** prefill the peer with your own root-cause analysis, proposed fix, or leading questions — see the *Independent Judgment Rule* below. The handoff card is for context, not conclusions.
|
|
108
109
|
|
|
109
110
|
### Anti-Hallucination Rule
|
|
110
111
|
|
|
111
112
|
When mentioning specific paths, function names, commands, line numbers, or tool results, **must cite the source** ("I read X at line Y"). If unverified, state "unverified" explicitly.
|
|
112
113
|
|
|
114
|
+
### Independent Judgment Rule (round=1)
|
|
115
|
+
|
|
116
|
+
The whole point of peer review is to surface a **second, independent** read. If
|
|
117
|
+
the reviewer's own root-cause analysis, fix diff, and leading questions are sent
|
|
118
|
+
to the peer up front, the peer can only AGREE inside the reviewer's frame — and
|
|
119
|
+
that AGREE carries no signal. The reviewer **must complete their own analysis
|
|
120
|
+
before opening round=1**; skipping that step turns peer review into a search for
|
|
121
|
+
endorsement.
|
|
122
|
+
|
|
123
|
+
Round=1 message **must NOT include**:
|
|
124
|
+
|
|
125
|
+
- The reviewer's own root-cause analysis ("the bug is in function X at line Y because…").
|
|
126
|
+
- The reviewer's own proposed fix, patch, or diff.
|
|
127
|
+
- Leading questions of the form "do you agree with my conclusion on X?" / "is the change I made on Y safe?" — these lock the peer into the reviewer's framing.
|
|
128
|
+
- Specific line numbers, function names, or branch points the reviewer has already identified as relevant — let the peer locate them.
|
|
129
|
+
|
|
130
|
+
Round=1 message **should include**:
|
|
131
|
+
|
|
132
|
+
- The Project Handoff Card (above).
|
|
133
|
+
- Symptoms exactly as observed: the user's reported error, terminal output verbatim, the precise commands that triggered it.
|
|
134
|
+
- Necessary external context: the goal of the work, the date / version under test, anything the peer cannot infer from the repo alone.
|
|
135
|
+
- Key file pointers as **entry points** (paths only — let the peer choose what to read and how deep).
|
|
136
|
+
- An open invitation: "independently identify the root cause, propose a fix, and call out any test gaps."
|
|
137
|
+
|
|
138
|
+
After receiving the peer's round=1 reply, the reviewer **compares** their own
|
|
139
|
+
conclusion to the peer's and routes the next action:
|
|
140
|
+
|
|
141
|
+
| Reviewer's own conclusion vs. peer's conclusion | Next action |
|
|
142
|
+
|---|---|
|
|
143
|
+
| Same root cause + same fix direction | High confidence — AGREE and proceed to execution |
|
|
144
|
+
| Same root cause, different fix direction | REFINE — open round=2 to reconcile the fix |
|
|
145
|
+
| Different root cause | OBJECT — open round=2; at least one of the two analyses is wrong |
|
|
146
|
+
| Peer asks for more context | REFINE — supply the missing context, then re-evaluate |
|
|
147
|
+
|
|
148
|
+
#### Example (bad — endorsement-seeking)
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Bug is in `cmd_init` at line 932 — the v2 demo renderer fires unconditionally.
|
|
152
|
+
My fix: gate it behind `--demo`. Q1: is this over-killed? Q2: should I
|
|
153
|
+
refactor the renderer instead? Q3: are the tests strong enough?
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The peer can only AGREE or quibble inside the reviewer's framing.
|
|
157
|
+
|
|
158
|
+
#### Example (good — independent analysis)
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Symptoms: user ran `roll init` on /path/X and saw [verbatim terminal output A];
|
|
162
|
+
then ran `roll backlog` and saw [verbatim terminal output B]. Project background:
|
|
163
|
+
[project shape]. Entry points: `bin/roll`, `lib/roll-init.py`, `tests/`.
|
|
164
|
+
Independently identify the root cause and propose a fix.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The peer reads, locates, and proposes on its own. The reviewer then compares.
|
|
168
|
+
|
|
113
169
|
## State Machine
|
|
114
170
|
|
|
115
171
|
### Per Negotiation (Single Task)
|
|
@@ -208,8 +264,10 @@ When peer review is manually triggered by a human (via `/peer`, "叫上 peer", e
|
|
|
208
264
|
|
|
209
265
|
**Rules:**
|
|
210
266
|
- Peer CLI calls must be **synchronous** (do NOT use background/async execution).
|
|
267
|
+
- The outgoing round=1 message must follow the *Independent Judgment Rule* above — no root-cause analysis, no fix diff, no leading questions.
|
|
211
268
|
- Show the outgoing message **before** calling the peer, so the user sees what's being asked.
|
|
212
269
|
- Relay the peer's response **verbatim** before adding your own analysis.
|
|
270
|
+
- After the peer's reply, the reviewer's own analysis block must explicitly state whether the peer's root cause and fix direction match the reviewer's own (independent) conclusion — that comparison is what determines the next round's action.
|
|
213
271
|
- If a peer call fails or times out, report it immediately inline and either retry or ESCALATE.
|
|
214
272
|
- Negotiation log is still written to `~/.shared/roll/peer/logs/` as usual.
|
|
215
273
|
|