@seanyao/roll 2026.523.1 → 2026.523.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/bin/roll +418 -14
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/i18n.sh +113 -0
- package/lib/loop-fmt.py +45 -0
- package/lib/model_prices.py +78 -38
- package/lib/prices/snapshot-2026-05-22.json +20 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-loop-status.py +43 -19
- package/lib/roll_render.py +6 -1
- package/package.json +1 -1
package/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.523.
|
|
7
|
+
VERSION="2026.523.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"
|
|
@@ -21,6 +21,10 @@ SCRIPT_DIR="$(cd "$(dirname "$_source")" && pwd)"
|
|
|
21
21
|
ROLL_PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
22
22
|
ROLL_PKG_CONVENTIONS="${ROLL_PKG_DIR}/conventions"
|
|
23
23
|
|
|
24
|
+
# US-I18N-001: i18n engine (locale resolution + message catalog).
|
|
25
|
+
# shellcheck source=../lib/i18n.sh
|
|
26
|
+
[[ -f "${ROLL_PKG_DIR}/lib/i18n.sh" ]] && source "${ROLL_PKG_DIR}/lib/i18n.sh"
|
|
27
|
+
|
|
24
28
|
# Colors
|
|
25
29
|
RED=$'\033[0;31m'
|
|
26
30
|
GREEN=$'\033[0;32m'
|
|
@@ -3620,6 +3624,107 @@ EOF
|
|
|
3620
3624
|
return "$rc"
|
|
3621
3625
|
}
|
|
3622
3626
|
|
|
3627
|
+
# ─── cmd_prices (US-VIEW-013) ─────────────────────────────────────────────────
|
|
3628
|
+
# `roll prices show` — print the current price snapshot table.
|
|
3629
|
+
# `roll prices refresh` — fetch live pricing docs, diff vs latest snapshot,
|
|
3630
|
+
# write a new snapshot when rates have changed.
|
|
3631
|
+
_prices_help() {
|
|
3632
|
+
cat <<'EOF'
|
|
3633
|
+
Usage: roll prices <subcommand> [--url URL]
|
|
3634
|
+
roll prices <子命令> [--url 网址]
|
|
3635
|
+
|
|
3636
|
+
Subcommands:
|
|
3637
|
+
show Print the current price snapshot table.
|
|
3638
|
+
显示当前价格快照表。
|
|
3639
|
+
refresh Fetch the official pricing docs, diff against the latest snapshot,
|
|
3640
|
+
and write a new snapshot only when rates have changed.
|
|
3641
|
+
拉取官方价格文档与最新快照对比,有变化才落新快照。
|
|
3642
|
+
EOF
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
cmd_prices_show() {
|
|
3646
|
+
local lib_dir="${ROLL_PKG_DIR}/lib"
|
|
3647
|
+
python3 - "$lib_dir" <<'PY'
|
|
3648
|
+
import json, os, sys
|
|
3649
|
+
lib_dir = sys.argv[1]
|
|
3650
|
+
sys.path.insert(0, lib_dir)
|
|
3651
|
+
import model_prices as mp
|
|
3652
|
+
|
|
3653
|
+
version, effective_at, source_url = mp.snapshot_meta()
|
|
3654
|
+
print(f"price snapshot 价格快照")
|
|
3655
|
+
print(f" version {version}")
|
|
3656
|
+
print(f" effective_at {effective_at}")
|
|
3657
|
+
print(f" source {source_url}")
|
|
3658
|
+
print(f" default_model {mp.DEFAULT}")
|
|
3659
|
+
print()
|
|
3660
|
+
print(f" {'model':<22}{'in':>8}{'out':>8}{'cw':>8}{'cr':>8}")
|
|
3661
|
+
for model in sorted(mp.PRICES):
|
|
3662
|
+
p = mp.PRICES[model]
|
|
3663
|
+
print(f" {model:<22}{p['in']:>8.2f}{p['out']:>8.2f}{p['cache_create']:>8.2f}{p['cache_read']:>8.2f}")
|
|
3664
|
+
print()
|
|
3665
|
+
print("rates per million tokens, USD 单位为每百万 token 美元")
|
|
3666
|
+
PY
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
cmd_prices_refresh() {
|
|
3670
|
+
local url=""
|
|
3671
|
+
while (( $# > 0 )); do
|
|
3672
|
+
case "$1" in
|
|
3673
|
+
--url) url="$2"; shift 2 ;;
|
|
3674
|
+
*) err "Unknown flag: $1 未知参数: $1"; return 1 ;;
|
|
3675
|
+
esac
|
|
3676
|
+
done
|
|
3677
|
+
local lib_dir="${ROLL_PKG_DIR}/lib"
|
|
3678
|
+
python3 - "$lib_dir" "$url" <<'PY'
|
|
3679
|
+
import os, sys
|
|
3680
|
+
lib_dir = sys.argv[1]
|
|
3681
|
+
override_url = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
3682
|
+
sys.path.insert(0, lib_dir)
|
|
3683
|
+
import prices_fetcher as pf
|
|
3684
|
+
|
|
3685
|
+
snapshot_dir = os.path.join(lib_dir, "prices")
|
|
3686
|
+
url = override_url or pf.DEFAULT_SOURCE_URL
|
|
3687
|
+
|
|
3688
|
+
try:
|
|
3689
|
+
action, changes = pf.refresh(snapshot_dir=snapshot_dir, url=url)
|
|
3690
|
+
except pf.FetchError as exc:
|
|
3691
|
+
print(f"[roll] fetch failed: {exc}", file=sys.stderr)
|
|
3692
|
+
print("[roll] keeping existing snapshot, no changes written 保留旧快照,未写入新文件",
|
|
3693
|
+
file=sys.stderr)
|
|
3694
|
+
sys.exit(2)
|
|
3695
|
+
except pf.ParseError as exc:
|
|
3696
|
+
print(f"[roll] parse failed: {exc}", file=sys.stderr)
|
|
3697
|
+
print("[roll] keeping existing snapshot, no changes written 保留旧快照,未写入新文件",
|
|
3698
|
+
file=sys.stderr)
|
|
3699
|
+
sys.exit(3)
|
|
3700
|
+
|
|
3701
|
+
kind, _, _ = (action + "::").split(":", 2)
|
|
3702
|
+
if action == "unchanged":
|
|
3703
|
+
print("[roll] up to date 价格快照已是最新")
|
|
3704
|
+
sys.exit(0)
|
|
3705
|
+
if kind == "first":
|
|
3706
|
+
print(f"[roll] baseline snapshot written 写入首份基线快照")
|
|
3707
|
+
elif kind == "written":
|
|
3708
|
+
print(f"[roll] new snapshot written 写入新版价格快照")
|
|
3709
|
+
print(pf.format_diff(changes, colored=sys.stdout.isatty()))
|
|
3710
|
+
PY
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
cmd_prices() {
|
|
3714
|
+
local subcmd="${1:-}"
|
|
3715
|
+
shift || true
|
|
3716
|
+
case "$subcmd" in
|
|
3717
|
+
show) cmd_prices_show "$@" ;;
|
|
3718
|
+
refresh) cmd_prices_refresh "$@" ;;
|
|
3719
|
+
--help|-h|help|"") _prices_help ;;
|
|
3720
|
+
*)
|
|
3721
|
+
err "Unknown subcommand: ${subcmd} 未知子命令:${subcmd}"
|
|
3722
|
+
_prices_help >&2
|
|
3723
|
+
return 1
|
|
3724
|
+
;;
|
|
3725
|
+
esac
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3623
3728
|
cmd_slides() {
|
|
3624
3729
|
local subcmd="${1:-}"
|
|
3625
3730
|
shift || true
|
|
@@ -4085,8 +4190,39 @@ _loop_event() {
|
|
|
4085
4190
|
esac
|
|
4086
4191
|
mkdir -p "$(dirname "$evfile")"
|
|
4087
4192
|
|
|
4088
|
-
#
|
|
4089
|
-
|
|
4193
|
+
# US-LOOP-007: human-friendly stdout for phase_* stages so tmux readers
|
|
4194
|
+
# spot phase boundaries amid claude output. Other stages keep the legacy
|
|
4195
|
+
# tab-separated format (consumers like tests grep on it).
|
|
4196
|
+
case "$stage" in
|
|
4197
|
+
phase_start)
|
|
4198
|
+
local _emoji
|
|
4199
|
+
case "$label" in
|
|
4200
|
+
startup) _emoji="🚀" ;;
|
|
4201
|
+
preflight) _emoji="🔍" ;;
|
|
4202
|
+
worktree_setup) _emoji="🌳" ;;
|
|
4203
|
+
claude_invoke) _emoji="🤖" ;;
|
|
4204
|
+
publish_push) _emoji="📤" ;;
|
|
4205
|
+
publish_wait_merge) _emoji="⏳" ;;
|
|
4206
|
+
cleanup) _emoji="🧹" ;;
|
|
4207
|
+
*) _emoji="▶" ;;
|
|
4208
|
+
esac
|
|
4209
|
+
printf '%s %-22s ─────────\n' "$_emoji" "$label"
|
|
4210
|
+
;;
|
|
4211
|
+
phase_tick)
|
|
4212
|
+
printf ' ⏱ %-20s ───── %s\n' "$label" "$detail"
|
|
4213
|
+
;;
|
|
4214
|
+
phase_end)
|
|
4215
|
+
local _mark
|
|
4216
|
+
case "$outcome" in
|
|
4217
|
+
fail) _mark="✗" ;;
|
|
4218
|
+
*) _mark="✓" ;;
|
|
4219
|
+
esac
|
|
4220
|
+
printf ' %s %-20s ───── %s\n' "$_mark" "$label" "$detail"
|
|
4221
|
+
;;
|
|
4222
|
+
*)
|
|
4223
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$ts" "$stage" "$label" "$detail" "$outcome"
|
|
4224
|
+
;;
|
|
4225
|
+
esac
|
|
4090
4226
|
|
|
4091
4227
|
# JSON line appended to NDJSON file. FIX-067: drop the flock/lockf guard.
|
|
4092
4228
|
# POSIX requires write() ≤ PIPE_BUF (≥512 bytes, 4 KiB on Linux/macOS) to
|
|
@@ -4321,9 +4457,47 @@ fi
|
|
|
4321
4457
|
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
4322
4458
|
# FIX-038: background heartbeat writer — outer script uses this as primary liveness signal
|
|
4323
4459
|
# to detect stale execution without relying on PID reuse heuristics.
|
|
4460
|
+
# US-LOOP-007: heartbeat also emits phase_tick for the current phase so tmux
|
|
4461
|
+
# readers see "still alive in <phase>" during long-running silences (e.g.
|
|
4462
|
+
# claude_invoke 5-45 min). CURRENT_PHASE is maintained by _phase_begin/_phase_end.
|
|
4324
4463
|
HEARTBEAT_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
|
|
4464
|
+
CURRENT_PHASE=""
|
|
4465
|
+
# bash 3.2 (macOS /bin/bash) lacks associative arrays — use namespaced
|
|
4466
|
+
# variables via 'printf -v' + indirect '\${!VAR}' expansion instead.
|
|
4467
|
+
# _PHASE_START_<name> stores the unix-second start ts per phase
|
|
4468
|
+
# _PHASE_DUR_<name> stores the computed duration in seconds per phase
|
|
4469
|
+
# _PHASE_NAMES_DONE space-separated list of completed phase names, in order
|
|
4470
|
+
_PHASE_NAMES_DONE=""
|
|
4471
|
+
_phase_begin() {
|
|
4472
|
+
local _name="\$1"
|
|
4473
|
+
printf -v "_PHASE_START_\${_name}" '%s' "\$(date +%s)"
|
|
4474
|
+
CURRENT_PHASE="\$_name"
|
|
4475
|
+
_loop_event phase_start "\$_name" "" "" || true
|
|
4476
|
+
}
|
|
4477
|
+
_phase_end() {
|
|
4478
|
+
local _name="\$1" _outcome="\${2:-ok}"
|
|
4479
|
+
local _start_var="_PHASE_START_\${_name}"
|
|
4480
|
+
local _start="\${!_start_var:-\$(date +%s)}"
|
|
4481
|
+
local _dur=\$(( \$(date +%s) - _start ))
|
|
4482
|
+
[ "\$_dur" -lt 0 ] && _dur=0
|
|
4483
|
+
printf -v "_PHASE_DUR_\${_name}" '%s' "\$_dur"
|
|
4484
|
+
case " \$_PHASE_NAMES_DONE " in *" \$_name "*) ;; *) _PHASE_NAMES_DONE="\${_PHASE_NAMES_DONE} \$_name" ;; esac
|
|
4485
|
+
CURRENT_PHASE=""
|
|
4486
|
+
_loop_event phase_end "\$_name" "\${_dur}s" "\$_outcome" || true
|
|
4487
|
+
}
|
|
4325
4488
|
_heartbeat_writer() {
|
|
4326
|
-
while true; do
|
|
4489
|
+
while true; do
|
|
4490
|
+
echo "\$(date -u +%s)" > "\$HEARTBEAT_FILE"
|
|
4491
|
+
if [ -n "\$CURRENT_PHASE" ]; then
|
|
4492
|
+
local _start_var="_PHASE_START_\${CURRENT_PHASE}"
|
|
4493
|
+
local _phase_start="\${!_start_var:-}"
|
|
4494
|
+
if [ -n "\$_phase_start" ]; then
|
|
4495
|
+
local _el=\$(( \$(date +%s) - _phase_start ))
|
|
4496
|
+
_loop_event phase_tick "\$CURRENT_PHASE" "\${_el}s elapsed" "" 2>/dev/null || true
|
|
4497
|
+
fi
|
|
4498
|
+
fi
|
|
4499
|
+
sleep 60
|
|
4500
|
+
done
|
|
4327
4501
|
}
|
|
4328
4502
|
_heartbeat_writer &
|
|
4329
4503
|
_HEARTBEAT_PID=\$!
|
|
@@ -4343,8 +4517,65 @@ trap '_on_sigterm' TERM
|
|
|
4343
4517
|
# US-LOOP-005: idempotent runs.jsonl writer shared by normal exit, timeout
|
|
4344
4518
|
# trap, and worktree-setup-failure early exit. Guards on jq + run_id dedupe so
|
|
4345
4519
|
# multiple callers in the same cycle are safe.
|
|
4520
|
+
# US-LOOP-008: build a JSON object {"<phase>": <duration_sec>, ...} from
|
|
4521
|
+
# the ordered list of completed phases. Returns "{}" if no phases ran.
|
|
4522
|
+
_phases_to_json() {
|
|
4523
|
+
command -v jq >/dev/null 2>&1 || { printf '{}'; return 0; }
|
|
4524
|
+
local _name _var _dur _args="" _filter="{}"
|
|
4525
|
+
local _first=1
|
|
4526
|
+
for _name in \$_PHASE_NAMES_DONE; do
|
|
4527
|
+
[ -z "\$_name" ] && continue
|
|
4528
|
+
_var="_PHASE_DUR_\${_name}"
|
|
4529
|
+
_dur="\${!_var:-0}"
|
|
4530
|
+
if [ "\$_first" -eq 1 ]; then
|
|
4531
|
+
_filter="{\\"\${_name}\\": \\\$d_\${_name}}"
|
|
4532
|
+
_first=0
|
|
4533
|
+
else
|
|
4534
|
+
_filter="\${_filter} + {\\"\${_name}\\": \\\$d_\${_name}}"
|
|
4535
|
+
fi
|
|
4536
|
+
_args="\${_args} --argjson d_\${_name} \${_dur}"
|
|
4537
|
+
done
|
|
4538
|
+
if [ "\$_first" -eq 1 ]; then
|
|
4539
|
+
printf '{}'
|
|
4540
|
+
else
|
|
4541
|
+
eval "jq -nc \${_args} '\${_filter}'" 2>/dev/null || printf '{}'
|
|
4542
|
+
fi
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
# US-LOOP-008: print phase breakdown panel sorted by duration desc.
|
|
4546
|
+
# Idle/failed/aborted cycles only list phases that actually entered — no
|
|
4547
|
+
# placeholder rows. Panel is best-effort (skips silently if no phases).
|
|
4548
|
+
_print_phase_breakdown() {
|
|
4549
|
+
[ -n "\$_PHASE_NAMES_DONE" ] || return 0
|
|
4550
|
+
local _name _var _dur _total=0 _rows=""
|
|
4551
|
+
for _name in \$_PHASE_NAMES_DONE; do
|
|
4552
|
+
[ -z "\$_name" ] && continue
|
|
4553
|
+
_var="_PHASE_DUR_\${_name}"
|
|
4554
|
+
_dur="\${!_var:-0}"
|
|
4555
|
+
_total=\$(( _total + _dur ))
|
|
4556
|
+
_rows="\${_rows}\${_dur} \${_name}\n"
|
|
4557
|
+
done
|
|
4558
|
+
[ "\$_total" -le 0 ] && _total=1
|
|
4559
|
+
printf '\\n─── Cycle %s Phase Breakdown ───\\n' "\${CYCLE_ID:-unknown}"
|
|
4560
|
+
printf '%b' "\$_rows" | sort -rn | while read -r _d _n; do
|
|
4561
|
+
[ -z "\$_n" ] && continue
|
|
4562
|
+
local _pct=\$(( (_d * 1000) / _total ))
|
|
4563
|
+
local _pct_str
|
|
4564
|
+
_pct_str=\$(printf '%d.%d%%' \$(( _pct / 10 )) \$(( _pct % 10 )))
|
|
4565
|
+
local _bar="" _bar_len=\$(( (_d * 20) / _total ))
|
|
4566
|
+
[ "\$_bar_len" -gt 0 ] && _bar=\$(printf '█%.0s' \$(seq 1 \$_bar_len))
|
|
4567
|
+
printf ' %-22s %6ds (%6s) %s\\n' "\$_n" "\$_d" "\$_pct_str" "\$_bar"
|
|
4568
|
+
done
|
|
4569
|
+
printf ' %s\\n' "──────────────────────────────────────"
|
|
4570
|
+
printf ' %-22s %6ds\\n\\n' "Total" "\$_total"
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4346
4573
|
_runs_append() {
|
|
4347
4574
|
local _status="\$1"; local _tcr="\${2:-0}"; local _built="\${3:-[]}"
|
|
4575
|
+
# bash parameter expansion \${4:-{}} stops at the first \} so the default
|
|
4576
|
+
# leaks a trailing \} into a real 4th arg ("{...}}"). Test explicit empty.
|
|
4577
|
+
local _phases_json="\${4:-}"
|
|
4578
|
+
[ -z "\$_phases_json" ] && _phases_json="{}"
|
|
4348
4579
|
local _runs_dst="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/runs.jsonl"
|
|
4349
4580
|
command -v jq >/dev/null 2>&1 || return 0
|
|
4350
4581
|
local _cid="\${CYCLE_ID:-pre-cycle-\$\$}"
|
|
@@ -4366,14 +4597,21 @@ _runs_append() {
|
|
|
4366
4597
|
--argjson alerts '[]' \\
|
|
4367
4598
|
--argjson tcr_count "\$_tcr" \\
|
|
4368
4599
|
--argjson duration_sec "\$_dur" \\
|
|
4600
|
+
--argjson phases "\$_phases_json" \\
|
|
4369
4601
|
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
4370
4602
|
cycle_id:\$cycle_id,
|
|
4371
4603
|
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
4372
|
-
tcr_count:\$tcr_count, duration_sec:\$duration_sec}' \\
|
|
4604
|
+
tcr_count:\$tcr_count, duration_sec:\$duration_sec, phases:\$phases}' \\
|
|
4373
4605
|
>> "\$_runs_dst" 2>/dev/null || true
|
|
4374
4606
|
}
|
|
4375
4607
|
_inner_cleanup() {
|
|
4376
4608
|
local _rc=\$?
|
|
4609
|
+
# US-LOOP-007: close any CURRENT_PHASE that wasn't ended explicitly
|
|
4610
|
+
# (sigterm / set -e fire / aborted exit paths). Marks as fail so the
|
|
4611
|
+
# breakdown panel shows where we died.
|
|
4612
|
+
if [ -n "\${CURRENT_PHASE:-}" ]; then
|
|
4613
|
+
_phase_end "\$CURRENT_PHASE" fail 2>/dev/null || true
|
|
4614
|
+
fi
|
|
4377
4615
|
# Kill heartbeat + every remaining background job (watchdog, orphan
|
|
4378
4616
|
# loop-fmt.py, publish subshells) — bash's foreground 'wait' for the
|
|
4379
4617
|
# pipe has already returned by the time the EXIT trap runs.
|
|
@@ -4385,7 +4623,8 @@ _inner_cleanup() {
|
|
|
4385
4623
|
# US-LOOP-005 T9: timeout path must also write runs.jsonl row so dashboard
|
|
4386
4624
|
# has a terminal record (cycle_end alone is insufficient — runs.jsonl is
|
|
4387
4625
|
# the canonical history feed for 'roll loop runs').
|
|
4388
|
-
|
|
4626
|
+
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
4627
|
+
_runs_append "failed" 0 "[]" "\$_phases_t" 2>/dev/null || true
|
|
4389
4628
|
_worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
|
|
4390
4629
|
fi
|
|
4391
4630
|
# FIX-086: aborted-path orphan safety net. When the inner script is killed
|
|
@@ -4430,7 +4669,8 @@ _inner_cleanup() {
|
|
|
4430
4669
|
_loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "done" 2>/dev/null || true
|
|
4431
4670
|
_CYCLE_END_WRITTEN=1
|
|
4432
4671
|
# FIX-099: pass real tcr_count + built[] instead of 0/"[]"
|
|
4433
|
-
|
|
4672
|
+
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
4673
|
+
_runs_append "done" "\${_orphan_tcr}" "\${_orphan_built}" "\$_phases_t" 2>/dev/null || true
|
|
4434
4674
|
# FIX-099: three-field ALERT so callers can distinguish recovered orphan
|
|
4435
4675
|
# from a cycle's normally-picked story (was: "FIX-091 published as PR"
|
|
4436
4676
|
# which leaked a hardcoded string regardless of what was actually built).
|
|
@@ -4443,7 +4683,8 @@ _inner_cleanup() {
|
|
|
4443
4683
|
_loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "orphan" 2>/dev/null || true
|
|
4444
4684
|
_CYCLE_END_WRITTEN=1
|
|
4445
4685
|
# FIX-099: pass real tcr_count + built[] for the orphan-tag path too
|
|
4446
|
-
|
|
4686
|
+
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
4687
|
+
_runs_append "orphan" "\${_orphan_tcr}" "\${_orphan_built}" "\$_phases_t" 2>/dev/null || true
|
|
4447
4688
|
_worktree_alert "cycle \${CYCLE_ID}: recovered_from_orphan=yes; tcr_commits=\${_orphan_tcr}; stories=\${_orphan_built}; FIX-086 pushed orphan tag \${_orphan_tag}" 2>/dev/null || true
|
|
4448
4689
|
fi
|
|
4449
4690
|
fi
|
|
@@ -4455,7 +4696,8 @@ _inner_cleanup() {
|
|
|
4455
4696
|
# "still running" until the next successful cycle rolls past it.
|
|
4456
4697
|
if [ "\${_CYCLE_END_WRITTEN:-0}" -eq 0 ] && [ -n "\${CYCLE_ID:-}" ]; then
|
|
4457
4698
|
_loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "aborted" 2>/dev/null || true
|
|
4458
|
-
|
|
4699
|
+
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
4700
|
+
_runs_append "aborted" 0 "[]" "\$_phases_t" 2>/dev/null || true
|
|
4459
4701
|
fi
|
|
4460
4702
|
rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
|
|
4461
4703
|
exit "\$_rc"
|
|
@@ -4494,6 +4736,11 @@ CYCLE_START=\$(date +%s)
|
|
|
4494
4736
|
WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
4495
4737
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
4496
4738
|
_USE_WORKTREE=0
|
|
4739
|
+
# US-LOOP-007: startup phase covers env / lock / heartbeat setup. End it now
|
|
4740
|
+
# that cycle vars are bound and we're about to do real work.
|
|
4741
|
+
_phase_begin startup
|
|
4742
|
+
_phase_end startup ok
|
|
4743
|
+
_phase_begin preflight
|
|
4497
4744
|
cd "${project_path}" 2>/dev/null || true
|
|
4498
4745
|
# FIX-104: GC stale merged temp branches at cycle entry — before worktree setup
|
|
4499
4746
|
# and before any early-exit gate (pre-run abort, CI red precheck). The post-claude
|
|
@@ -4536,6 +4783,8 @@ done
|
|
|
4536
4783
|
# US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
|
|
4537
4784
|
# post-claude cleanup can diff and delete only this session's additions.
|
|
4538
4785
|
CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
|
|
4786
|
+
_phase_end preflight ok
|
|
4787
|
+
_phase_begin worktree_setup
|
|
4539
4788
|
if _worktree_fetch_origin main \\
|
|
4540
4789
|
&& _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
|
|
4541
4790
|
_USE_WORKTREE=1
|
|
@@ -4547,16 +4796,19 @@ if _worktree_fetch_origin main \\
|
|
|
4547
4796
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
4548
4797
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
4549
4798
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
4799
|
+
_phase_end worktree_setup ok
|
|
4550
4800
|
else
|
|
4551
4801
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
4552
4802
|
# --dangerously-skip-permissions is only safe paired with worktree isolation;
|
|
4553
4803
|
# falling back to the main tree without isolation is unacceptable.
|
|
4554
4804
|
_worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
|
|
4555
4805
|
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
|
|
4806
|
+
_phase_end worktree_setup fail
|
|
4556
4807
|
# US-LOOP-005 T10: worktree-setup-failed path leaves no commits and never
|
|
4557
4808
|
# emits cycle_start, but dashboard still needs a runs.jsonl row marking the
|
|
4558
4809
|
# cycle as failed (otherwise the scheduled tick appears to have vanished).
|
|
4559
|
-
|
|
4810
|
+
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
4811
|
+
_runs_append "failed" 0 "[]" "\$_phases_t" 2>/dev/null || true
|
|
4560
4812
|
exit 0
|
|
4561
4813
|
fi
|
|
4562
4814
|
|
|
@@ -4568,6 +4820,7 @@ FMT="${fmt_script}"
|
|
|
4568
4820
|
export LOOP_PROJECT_SLUG="${slug}"
|
|
4569
4821
|
export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
4570
4822
|
export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
4823
|
+
_phase_begin claude_invoke
|
|
4571
4824
|
for _attempt in 1 2 3; do
|
|
4572
4825
|
# FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
|
|
4573
4826
|
# the SIGTERM result of the previous attempt and would otherwise force an
|
|
@@ -4605,6 +4858,12 @@ for _attempt in 1 2 3; do
|
|
|
4605
4858
|
fi
|
|
4606
4859
|
done
|
|
4607
4860
|
|
|
4861
|
+
if [ "\$_CYCLE_TIMED_OUT" -eq 1 ] || [ "\$_exit" -ne 0 ]; then
|
|
4862
|
+
_phase_end claude_invoke fail
|
|
4863
|
+
else
|
|
4864
|
+
_phase_end claude_invoke ok
|
|
4865
|
+
fi
|
|
4866
|
+
|
|
4608
4867
|
# FIX-057: timed out — skip publish; EXIT trap writes cycle_end blocked + ALERT.
|
|
4609
4868
|
if [ "\$_CYCLE_TIMED_OUT" -eq 1 ]; then
|
|
4610
4869
|
echo "[loop] cycle \${CYCLE_ID}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — aborting cycle (worktree preserved at \$WT)"
|
|
@@ -4653,6 +4912,7 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
4653
4912
|
else
|
|
4654
4913
|
_is_doc_only=0
|
|
4655
4914
|
( cd "\$WT" && _loop_is_doc_only_change ) && _is_doc_only=1
|
|
4915
|
+
_phase_begin publish_push
|
|
4656
4916
|
if [ "\$_is_doc_only" -eq 1 ]; then
|
|
4657
4917
|
( cd "\$WT" && _loop_publish_doc_pr "\$BRANCH" "doc: loop cycle \${CYCLE_ID}" )
|
|
4658
4918
|
else
|
|
@@ -4660,18 +4920,25 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
4660
4920
|
fi
|
|
4661
4921
|
_publish_status=\$?
|
|
4662
4922
|
if [ "\$_publish_status" -eq 0 ]; then
|
|
4923
|
+
_phase_end publish_push ok
|
|
4663
4924
|
# FIX-047: CI green ≠ delivered — wait for actual PR merge before cycle complete
|
|
4664
4925
|
if [ "\$_is_doc_only" -eq 0 ]; then
|
|
4926
|
+
_phase_begin publish_wait_merge
|
|
4665
4927
|
if ! ( cd "\$WT" && _loop_wait_pr_merge "\$BRANCH" ); then
|
|
4666
4928
|
_worktree_alert "cycle \${CYCLE_ID}: FIX-047: PR not merged within timeout — code may not be in main (BRANCH=\${BRANCH})"
|
|
4929
|
+
_phase_end publish_wait_merge fail
|
|
4930
|
+
else
|
|
4931
|
+
_phase_end publish_wait_merge ok
|
|
4667
4932
|
fi
|
|
4668
4933
|
fi
|
|
4934
|
+
_phase_begin cleanup
|
|
4669
4935
|
# US-VIEW-011: emit terminal PR state (merged/closed/open) before cycle_end
|
|
4670
4936
|
# so dashboard renders #NN ✓/↩/… correctly. Must run while branch ref
|
|
4671
4937
|
# is still resolvable on remote — gh pr view <branch> needs the head ref.
|
|
4672
4938
|
_loop_emit_pr_final "\$BRANCH" 2>/dev/null || true
|
|
4673
4939
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
4674
4940
|
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
4941
|
+
_phase_end cleanup ok
|
|
4675
4942
|
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
4676
4943
|
elif [ "\$_publish_status" -eq 2 ]; then
|
|
4677
4944
|
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
@@ -4726,11 +4993,15 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
4726
4993
|
fi
|
|
4727
4994
|
fi
|
|
4728
4995
|
|
|
4996
|
+
# US-LOOP-008: cycle Phase Breakdown panel — printed to stdout (tmux readers)
|
|
4997
|
+
# before runs.jsonl is appended, so the user sees timings even if jq is missing.
|
|
4998
|
+
_print_phase_breakdown 2>/dev/null || true
|
|
4999
|
+
_phases_for_runs=\$(_phases_to_json 2>/dev/null || echo '{}')
|
|
4729
5000
|
# FIX-044 / Step 5: Write loop cycle run summary to runs.jsonl
|
|
4730
5001
|
# Deterministic — runs in shell regardless of whether agent executes SKILL.md Step 5.
|
|
4731
5002
|
# US-LOOP-005: now routed through _runs_append so timeout/worktree-setup-fail
|
|
4732
5003
|
# share the same write logic. _runs_append is idempotent on run_id.
|
|
4733
|
-
_runs_append "\$_cycle_status" "\$_cycle_tcr" "\$_cycle_built" 2>/dev/null || true
|
|
5004
|
+
_runs_append "\$_cycle_status" "\$_cycle_tcr" "\$_cycle_built" "\$_phases_for_runs" 2>/dev/null || true
|
|
4734
5005
|
INNER
|
|
4735
5006
|
chmod +x "$inner_path"
|
|
4736
5007
|
|
|
@@ -5453,6 +5724,71 @@ _loop_attach() {
|
|
|
5453
5724
|
}
|
|
5454
5725
|
|
|
5455
5726
|
# Pretty-print a duration in seconds as "Xs" / "Ym" / "Yh Zm".
|
|
5727
|
+
# US-VIEW-019: compute slowest phase + % from a JSON line's phases object.
|
|
5728
|
+
# Returns "<abbr> <pct>%" (e.g. "claude 97%") or empty when no phases data.
|
|
5729
|
+
# Abbreviations match the AC: claude_invoke→claude, publish_wait_merge→pr-wait,
|
|
5730
|
+
# publish_push→publish, worktree_setup→worktree; others unchanged.
|
|
5731
|
+
_loop_runs_slowest_phase() {
|
|
5732
|
+
local line="$1"
|
|
5733
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
5734
|
+
local total max_name max_dur
|
|
5735
|
+
total=$(jq -r '(.phases // {}) | to_entries | map(.value) | add // 0' <<<"$line")
|
|
5736
|
+
[ -z "$total" ] || [ "$total" = "0" ] || [ "$total" = "null" ] && return 0
|
|
5737
|
+
max_name=$(jq -r '(.phases // {}) | to_entries | sort_by(-.value) | .[0].key // ""' <<<"$line")
|
|
5738
|
+
max_dur=$(jq -r '(.phases // {}) | to_entries | sort_by(-.value) | .[0].value // 0' <<<"$line")
|
|
5739
|
+
[ -z "$max_name" ] && return 0
|
|
5740
|
+
local abbr="$max_name"
|
|
5741
|
+
case "$max_name" in
|
|
5742
|
+
claude_invoke) abbr="claude" ;;
|
|
5743
|
+
publish_wait_merge) abbr="pr-wait" ;;
|
|
5744
|
+
publish_push) abbr="publish" ;;
|
|
5745
|
+
worktree_setup) abbr="worktree" ;;
|
|
5746
|
+
esac
|
|
5747
|
+
local pct=$(( (max_dur * 100 + total / 2) / total ))
|
|
5748
|
+
printf '%s %d%%' "$abbr" "$pct"
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
# US-VIEW-019: render the full Phase Breakdown panel for a cycle by re-using
|
|
5752
|
+
# the same shape that the inner runner script prints. Reads runs.jsonl,
|
|
5753
|
+
# locates the row by cycle_id, prints sorted-desc table.
|
|
5754
|
+
_loop_runs_detail() {
|
|
5755
|
+
local cycle_id="$1"
|
|
5756
|
+
command -v jq >/dev/null 2>&1 || { err "jq required for --detail"; return 1; }
|
|
5757
|
+
if [[ ! -f "$_LOOP_RUNS" ]]; then
|
|
5758
|
+
echo "No runs.jsonl yet 尚无运行记录"
|
|
5759
|
+
return 0
|
|
5760
|
+
fi
|
|
5761
|
+
local row
|
|
5762
|
+
row=$(jq -c --arg cid "$cycle_id" 'select(.cycle_id == $cid)' "$_LOOP_RUNS" | head -1)
|
|
5763
|
+
if [[ -z "$row" ]]; then
|
|
5764
|
+
echo "Cycle not found: $cycle_id 未找到对应 cycle"
|
|
5765
|
+
return 1
|
|
5766
|
+
fi
|
|
5767
|
+
local has_phases
|
|
5768
|
+
has_phases=$(jq -r '(.phases // {}) | length' <<<"$row")
|
|
5769
|
+
if [[ "$has_phases" == "0" ]]; then
|
|
5770
|
+
echo "Cycle $cycle_id has no phases data (pre-US-LOOP-008) 无 phase 数据"
|
|
5771
|
+
return 0
|
|
5772
|
+
fi
|
|
5773
|
+
echo ""
|
|
5774
|
+
echo "─── Cycle $cycle_id Phase Breakdown ───"
|
|
5775
|
+
local total
|
|
5776
|
+
total=$(jq -r '(.phases // {}) | to_entries | map(.value) | add // 0' <<<"$row")
|
|
5777
|
+
[ "$total" -le 0 ] && total=1
|
|
5778
|
+
jq -r '(.phases // {}) | to_entries | sort_by(-.value) | .[] | "\(.value) \(.key)"' <<<"$row" \
|
|
5779
|
+
| while read -r dur name; do
|
|
5780
|
+
[[ -z "$name" ]] && continue
|
|
5781
|
+
local pct=$(( (dur * 1000) / total ))
|
|
5782
|
+
local pct_str
|
|
5783
|
+
pct_str=$(printf '%d.%d%%' $((pct / 10)) $((pct % 10)))
|
|
5784
|
+
local bar="" bar_len=$(( (dur * 20) / total ))
|
|
5785
|
+
[ "$bar_len" -gt 0 ] && bar=$(printf '█%.0s' $(seq 1 "$bar_len"))
|
|
5786
|
+
printf ' %-22s %6ds (%6s) %s\n' "$name" "$dur" "$pct_str" "$bar"
|
|
5787
|
+
done
|
|
5788
|
+
echo " ──────────────────────────────────────"
|
|
5789
|
+
printf ' %-22s %6ds\n\n' "Total" "$total"
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5456
5792
|
_loop_runs_dur() {
|
|
5457
5793
|
local s="${1:-0}"
|
|
5458
5794
|
if [[ "$s" -lt 60 ]]; then printf "%ds" "$s"
|
|
@@ -5497,8 +5833,12 @@ _loop_runs_format_line() {
|
|
|
5497
5833
|
local skipped_note=""
|
|
5498
5834
|
[[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
|
|
5499
5835
|
local items_word; [[ "$built_count" -eq 1 ]] && items_word="item" || items_word="items"
|
|
5500
|
-
|
|
5501
|
-
|
|
5836
|
+
# US-VIEW-019: append slowest phase summary when phases data is present.
|
|
5837
|
+
local slowest_note=""
|
|
5838
|
+
local slowest_str; slowest_str=$(_loop_runs_slowest_phase "$line")
|
|
5839
|
+
[[ -n "$slowest_str" ]] && slowest_note=", slowest=${slowest_str}"
|
|
5840
|
+
printf " %s %s✅ built %d %s (%d tcr%s, %s%s)\n" \
|
|
5841
|
+
"$hhmm" "$prefix" "$built_count" "$items_word" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")" "$slowest_note"
|
|
5502
5842
|
local id desc
|
|
5503
5843
|
while IFS= read -r id; do
|
|
5504
5844
|
[[ -z "$id" ]] && continue
|
|
@@ -5535,15 +5875,23 @@ _loop_runs_format_line() {
|
|
|
5535
5875
|
|
|
5536
5876
|
# `roll loop runs [N] [--all]` — show recent loop iteration summaries.
|
|
5537
5877
|
_loop_runs() {
|
|
5538
|
-
local n=10 all_flag=false
|
|
5878
|
+
local n=10 all_flag=false detail_cycle=""
|
|
5539
5879
|
while [[ $# -gt 0 ]]; do
|
|
5540
5880
|
case "$1" in
|
|
5541
5881
|
--all) all_flag=true; shift ;;
|
|
5882
|
+
--detail) detail_cycle="${2:-}"; shift 2 ;;
|
|
5883
|
+
--detail=*) detail_cycle="${1#--detail=}"; shift ;;
|
|
5542
5884
|
[0-9]*) n="$1"; shift ;;
|
|
5543
5885
|
*) shift ;;
|
|
5544
5886
|
esac
|
|
5545
5887
|
done
|
|
5546
5888
|
|
|
5889
|
+
# US-VIEW-019: --detail <cycle_id> prints the Phase Breakdown panel.
|
|
5890
|
+
if [[ -n "$detail_cycle" ]]; then
|
|
5891
|
+
_loop_runs_detail "$detail_cycle"
|
|
5892
|
+
return $?
|
|
5893
|
+
fi
|
|
5894
|
+
|
|
5547
5895
|
# FIX-060: refresh merged status before reading, so paused-window merges show up.
|
|
5548
5896
|
_loop_backfill_merged >/dev/null 2>&1 || true
|
|
5549
5897
|
|
|
@@ -6872,6 +7220,9 @@ _loop_wait_pr_merge() {
|
|
|
6872
7220
|
local elapsed=0
|
|
6873
7221
|
local slug; _gh_resolve slug || return 0
|
|
6874
7222
|
while (( elapsed < timeout )); do
|
|
7223
|
+
# US-LOOP-007: emit phase_tick at the top of each poll iteration so the
|
|
7224
|
+
# tmux reader sees "still waiting" every 30s during merge wait.
|
|
7225
|
+
(( elapsed > 0 )) && _loop_event phase_tick publish_wait_merge "${elapsed}s elapsed" "" 2>/dev/null || true
|
|
6875
7226
|
local state; state=$(gh -R "$slug" pr view "$branch" --json state -q .state 2>/dev/null || echo "UNKNOWN")
|
|
6876
7227
|
case "$state" in
|
|
6877
7228
|
MERGED) return 0 ;;
|
|
@@ -7323,6 +7674,57 @@ cmd_alert() {
|
|
|
7323
7674
|
esac
|
|
7324
7675
|
}
|
|
7325
7676
|
|
|
7677
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
7678
|
+
# LANG — switch / inspect Roll's UI language (US-I18N-001)
|
|
7679
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
7680
|
+
|
|
7681
|
+
cmd_lang() {
|
|
7682
|
+
local arg="${1:-}"
|
|
7683
|
+
|
|
7684
|
+
case "$arg" in
|
|
7685
|
+
"")
|
|
7686
|
+
unset ROLL_LANG_RESOLVED
|
|
7687
|
+
local current src
|
|
7688
|
+
current=$(_i18n_resolve_lang)
|
|
7689
|
+
if [[ -n "${ROLL_LANG:-}" ]]; then
|
|
7690
|
+
src="ROLL_LANG env"
|
|
7691
|
+
elif [[ -f "$ROLL_CONFIG" ]] && grep -qE '^lang:' "$ROLL_CONFIG"; then
|
|
7692
|
+
src="config (${ROLL_CONFIG})"
|
|
7693
|
+
elif [[ -n "${LC_ALL:-}" || -n "${LANG:-}" ]]; then
|
|
7694
|
+
src="LC_ALL/LANG"
|
|
7695
|
+
else
|
|
7696
|
+
src="default"
|
|
7697
|
+
fi
|
|
7698
|
+
echo "current: ${current}, source: ${src}"
|
|
7699
|
+
;;
|
|
7700
|
+
zh|en)
|
|
7701
|
+
mkdir -p "$(dirname "$ROLL_CONFIG")"
|
|
7702
|
+
[[ -f "$ROLL_CONFIG" ]] || : > "$ROLL_CONFIG"
|
|
7703
|
+
local tmp; tmp=$(mktemp)
|
|
7704
|
+
grep -vE '^lang:' "$ROLL_CONFIG" > "$tmp" || true
|
|
7705
|
+
printf 'lang: %s\n' "$arg" >> "$tmp"
|
|
7706
|
+
mv "$tmp" "$ROLL_CONFIG"
|
|
7707
|
+
unset ROLL_LANG_RESOLVED
|
|
7708
|
+
ok "Language set to ${arg} 语言已设置为 ${arg}"
|
|
7709
|
+
;;
|
|
7710
|
+
--reset)
|
|
7711
|
+
if [[ -f "$ROLL_CONFIG" ]]; then
|
|
7712
|
+
local tmp; tmp=$(mktemp)
|
|
7713
|
+
grep -vE '^lang:' "$ROLL_CONFIG" > "$tmp" || true
|
|
7714
|
+
mv "$tmp" "$ROLL_CONFIG"
|
|
7715
|
+
fi
|
|
7716
|
+
unset ROLL_LANG_RESOLVED
|
|
7717
|
+
ok "Language preference cleared (will follow locale) 语言偏好已清除(跟随系统 locale)"
|
|
7718
|
+
;;
|
|
7719
|
+
*)
|
|
7720
|
+
err "Unknown language: ${arg} 未知语言: ${arg}"
|
|
7721
|
+
echo " Valid values: zh, en, --reset"
|
|
7722
|
+
echo " 可选值: zh, en, --reset"
|
|
7723
|
+
return 1
|
|
7724
|
+
;;
|
|
7725
|
+
esac
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7326
7728
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
7327
7729
|
|
|
7328
7730
|
cmd_ci() {
|
|
@@ -8117,11 +8519,13 @@ main() {
|
|
|
8117
8519
|
brief) cmd_brief "$@" ;;
|
|
8118
8520
|
backlog) cmd_backlog "$@" ;;
|
|
8119
8521
|
alert) cmd_alert "$@" ;;
|
|
8522
|
+
lang) cmd_lang "$@" ;;
|
|
8120
8523
|
agent) cmd_agent "$@" ;;
|
|
8121
8524
|
ci) cmd_ci "$@" ;;
|
|
8122
8525
|
doctor) cmd_doctor "$@" ;;
|
|
8123
8526
|
review-pr) cmd_review_pr "$@" ;;
|
|
8124
8527
|
slides) cmd_slides "$@" ;;
|
|
8528
|
+
prices) cmd_prices "$@" ;;
|
|
8125
8529
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
8126
8530
|
help|--help|-h) _help "$@" ;;
|
|
8127
8531
|
"") [[ -f ".roll/backlog.md" ]] && _home || { _help; _show_changelog; } ;;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|