@seanyao/roll 2026.512.2 → 2026.512.6
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 +9 -1
- package/bin/roll +174 -46
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v2026.512.6
|
|
4
|
+
- **Added**: peer review 现在也会自动弹出终端窗口,实时观察跨 AI 协商过程(mute 关闭同一开关)
|
|
5
|
+
- **Added**: `docs/guide/en/` — loop/dream/peer 英文用户指南上线,覆盖所有子命令和使用场景
|
|
6
|
+
- **Added**: `docs/guide/zh/` — loop/dream/peer 中文用户指南上线,内容与英文版语义一致
|
|
7
|
+
|
|
8
|
+
## v2026.512.5
|
|
9
|
+
- **Fixed**: loop 遇到 API 错误时自动重试,不再直接中断
|
|
10
|
+
|
|
11
|
+
## v2026.512.3
|
|
4
12
|
- **Added**: BACKLOG 支持 block / defer / unblock 状态管理 — 标记卡住的任务不再占队列
|
|
5
13
|
- **Fixed**: 自动弹窗现在能识别 Ghostty 和 iTerm2,不再强制弹出 Terminal.app
|
|
6
14
|
- **Fixed**: loop 检测到上一轮还在跑时自动跳过,不重复启动
|
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.512.
|
|
7
|
+
VERSION="2026.512.6"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -1422,9 +1422,70 @@ _peer_route() {
|
|
|
1422
1422
|
return 1
|
|
1423
1423
|
}
|
|
1424
1424
|
|
|
1425
|
+
# Open a terminal window attached to the given tmux session (peer auto-attach).
|
|
1426
|
+
# No-ops when muted, non-macOS, or tmux unavailable. Mirrors loop popup logic.
|
|
1427
|
+
_peer_auto_attach() {
|
|
1428
|
+
local session="$1"
|
|
1429
|
+
[ "$(uname)" = "Darwin" ] || return 0
|
|
1430
|
+
[ -f "$_LOOP_MUTE_FILE" ] && return 0
|
|
1431
|
+
local terminal_pref
|
|
1432
|
+
terminal_pref=$(_config_read_string "loop_attach_terminal" "")
|
|
1433
|
+
if [[ -z "$terminal_pref" ]]; then
|
|
1434
|
+
case "${TERM_PROGRAM:-}" in
|
|
1435
|
+
ghostty) terminal_pref="ghostty" ;;
|
|
1436
|
+
iTerm.app) terminal_pref="iTerm2" ;;
|
|
1437
|
+
*) terminal_pref="Terminal" ;;
|
|
1438
|
+
esac
|
|
1439
|
+
fi
|
|
1440
|
+
local launched=0
|
|
1441
|
+
if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
|
|
1442
|
+
open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
|
|
1443
|
+
fi
|
|
1444
|
+
if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
|
|
1445
|
+
osascript \
|
|
1446
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \
|
|
1447
|
+
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t $session\"" \
|
|
1448
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \
|
|
1449
|
+
&& launched=1 || true
|
|
1450
|
+
fi
|
|
1451
|
+
if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
|
|
1452
|
+
open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
|
|
1453
|
+
fi
|
|
1454
|
+
if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
|
|
1455
|
+
osascript \
|
|
1456
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \
|
|
1457
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t $session\"" \
|
|
1458
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
1459
|
+
fi
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
# Dispatch a peer CLI command inside an existing tmux session (window 0).
|
|
1463
|
+
# Writes stdout to out_file, stderr to err_file. Blocks until done or timeout.
|
|
1464
|
+
_peer_dispatch_in_tmux() {
|
|
1465
|
+
local session="$1" cmd_str="$2" out_file="$3" err_file="$4" timeout="${5:-180}"
|
|
1466
|
+
local done_file="${out_file}.done"
|
|
1467
|
+
local inner
|
|
1468
|
+
inner=$(mktemp /tmp/roll-peer-inner-XXXXXX.sh)
|
|
1469
|
+
{
|
|
1470
|
+
printf '#!/bin/bash -l\n'
|
|
1471
|
+
printf 'export PATH="/opt/homebrew/bin:$PATH"\n'
|
|
1472
|
+
printf '%s > %q 2> %q || true\n' "$cmd_str" "$out_file" "$err_file"
|
|
1473
|
+
printf 'touch %q\n' "$done_file"
|
|
1474
|
+
} > "$inner"
|
|
1475
|
+
chmod +x "$inner"
|
|
1476
|
+
tmux send-keys -t "${session}:0" "bash ${inner}; rm -f ${inner}" Enter
|
|
1477
|
+
local elapsed=0
|
|
1478
|
+
while [ ! -f "$done_file" ] && [ "$elapsed" -lt "$timeout" ]; do
|
|
1479
|
+
sleep 1
|
|
1480
|
+
elapsed=$((elapsed + 1))
|
|
1481
|
+
done
|
|
1482
|
+
rm -f "$done_file"
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1425
1485
|
_peer_call() {
|
|
1426
1486
|
local to="$1"
|
|
1427
1487
|
local prompt="$2"
|
|
1488
|
+
local session="${3:-}"
|
|
1428
1489
|
local output=""
|
|
1429
1490
|
local stderr_log
|
|
1430
1491
|
stderr_log="${_PEER_STATE_DIR}/logs/.last_stderr.log"
|
|
@@ -1433,30 +1494,50 @@ _peer_call() {
|
|
|
1433
1494
|
|
|
1434
1495
|
info "Peer call timeout: ${call_timeout}s Peer 调用超时: ${call_timeout}s"
|
|
1435
1496
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
;;
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
;;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1497
|
+
if [[ -n "$session" ]] && command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session" 2>/dev/null; then
|
|
1498
|
+
local out_file
|
|
1499
|
+
out_file=$(mktemp)
|
|
1500
|
+
local cmd_str
|
|
1501
|
+
case "$to" in
|
|
1502
|
+
claude) cmd_str="claude -p --output-format text $(printf %q "$prompt")" ;;
|
|
1503
|
+
kimi) cmd_str="kimi --quiet -p $(printf %q "$prompt")" ;;
|
|
1504
|
+
pi) cmd_str="pi -p $(printf %q "$prompt")" ;;
|
|
1505
|
+
deepseek) cmd_str="deepseek $(printf %q "$prompt")" ;;
|
|
1506
|
+
codex) cmd_str="codex exec --json --output-last-message $(printf %q "$prompt")" ;;
|
|
1507
|
+
opencode) cmd_str="opencode run $(printf %q "$prompt")" ;;
|
|
1508
|
+
*)
|
|
1509
|
+
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1510
|
+
return 1 ;;
|
|
1511
|
+
esac
|
|
1512
|
+
_peer_dispatch_in_tmux "$session" "$cmd_str" "$out_file" "$stderr_log" "$call_timeout"
|
|
1513
|
+
output="$(cat "$out_file" 2>/dev/null || true)"
|
|
1514
|
+
rm -f "$out_file"
|
|
1515
|
+
else
|
|
1516
|
+
case "$to" in
|
|
1517
|
+
claude)
|
|
1518
|
+
output="$(claude -p --output-format text "$prompt" 2>"$stderr_log" || true)"
|
|
1519
|
+
;;
|
|
1520
|
+
kimi)
|
|
1521
|
+
output="$(kimi --quiet -p "$prompt" 2>"$stderr_log" || true)"
|
|
1522
|
+
;;
|
|
1523
|
+
pi)
|
|
1524
|
+
output="$(pi -p "$prompt" 2>"$stderr_log" || true)"
|
|
1525
|
+
;;
|
|
1526
|
+
deepseek)
|
|
1527
|
+
output="$(deepseek "$prompt" 2>"$stderr_log" || true)"
|
|
1528
|
+
;;
|
|
1529
|
+
codex)
|
|
1530
|
+
output="$(codex exec --json --output-last-message "$prompt" 2>"$stderr_log" || true)"
|
|
1531
|
+
;;
|
|
1532
|
+
opencode)
|
|
1533
|
+
output="$(opencode run "$prompt" 2>"$stderr_log" || true)"
|
|
1534
|
+
;;
|
|
1535
|
+
*)
|
|
1536
|
+
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1537
|
+
return 1
|
|
1538
|
+
;;
|
|
1539
|
+
esac
|
|
1540
|
+
fi
|
|
1460
1541
|
|
|
1461
1542
|
printf '%s\n' "$output"
|
|
1462
1543
|
}
|
|
@@ -1582,6 +1663,15 @@ cmd_peer() {
|
|
|
1582
1663
|
local prompt
|
|
1583
1664
|
prompt="[PEER_REVIEW round=${round} tool=${from_tool}→${to_tool}]\n\n${context}"
|
|
1584
1665
|
|
|
1666
|
+
local peer_session=""
|
|
1667
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
1668
|
+
peer_session="roll-peer-${from_tool}-${to_tool}"
|
|
1669
|
+
if ! tmux has-session -t "$peer_session" 2>/dev/null; then
|
|
1670
|
+
tmux new-session -d -s "$peer_session" -x 200 -y 50
|
|
1671
|
+
_peer_auto_attach "$peer_session"
|
|
1672
|
+
fi
|
|
1673
|
+
fi
|
|
1674
|
+
|
|
1585
1675
|
_peer_ensure_state_dir
|
|
1586
1676
|
local log_file
|
|
1587
1677
|
log_file="${_PEER_STATE_DIR}/logs/$(date +%Y%m%d_%H%M%S)_${from_tool}_${to_tool}.md"
|
|
@@ -1606,7 +1696,7 @@ cmd_peer() {
|
|
|
1606
1696
|
|
|
1607
1697
|
info "Calling $to_tool... 调用 $to_tool..."
|
|
1608
1698
|
local response
|
|
1609
|
-
response="$(_peer_call "$to_tool" "$prompt")"
|
|
1699
|
+
response="$(_peer_call "$to_tool" "$prompt" "$peer_session")"
|
|
1610
1700
|
|
|
1611
1701
|
local stderr_log
|
|
1612
1702
|
stderr_log="${_PEER_STATE_DIR}/logs/.last_stderr.log"
|
|
@@ -1961,13 +2051,22 @@ _write_loop_runner_script() {
|
|
|
1961
2051
|
local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
|
|
1962
2052
|
cat > "$inner_path" << INNER
|
|
1963
2053
|
#!/bin/bash -l
|
|
2054
|
+
set -o pipefail
|
|
1964
2055
|
export PATH="/opt/homebrew/bin:\$PATH"
|
|
1965
2056
|
FMT="${fmt_script}"
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2057
|
+
for _attempt in 1 2 3; do
|
|
2058
|
+
if [ -f "\$FMT" ]; then
|
|
2059
|
+
( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
|
|
2060
|
+
else
|
|
2061
|
+
( cd "${project_path}" && ${cmd_verbose} )
|
|
2062
|
+
fi
|
|
2063
|
+
_exit=\$?
|
|
2064
|
+
[ "\$_exit" -eq 0 ] && break
|
|
2065
|
+
if [ "\$_attempt" -lt 3 ]; then
|
|
2066
|
+
echo "[loop] claude exited \$_exit (attempt \$_attempt/3) — retrying in 30s..."
|
|
2067
|
+
sleep 30
|
|
2068
|
+
fi
|
|
2069
|
+
done
|
|
1971
2070
|
INNER
|
|
1972
2071
|
chmod +x "$inner_path"
|
|
1973
2072
|
|
|
@@ -2013,21 +2112,19 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
2013
2112
|
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2014
2113
|
# Runtime terminal detection: try preferred first, fallback through installed apps.
|
|
2015
2114
|
# open -na returns non-zero when app not found, so || chain works as fallback.
|
|
2016
|
-
_PREF="${terminal_pref}"
|
|
2017
2115
|
_launched=0
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
fi
|
|
2116
|
+
case "${terminal_pref}" in
|
|
2117
|
+
ghostty|Ghostty)
|
|
2118
|
+
open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
|
|
2119
|
+
;;
|
|
2120
|
+
iTerm2|iTerm)
|
|
2121
|
+
osascript \\
|
|
2122
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2123
|
+
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
|
|
2124
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \\
|
|
2125
|
+
&& _launched=1 || true
|
|
2126
|
+
;;
|
|
2127
|
+
esac
|
|
2031
2128
|
if [ "\$_launched" -eq 0 ] && command -v osascript >/dev/null 2>&1; then
|
|
2032
2129
|
osascript \\
|
|
2033
2130
|
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
@@ -2404,6 +2501,7 @@ _loop_pause() {
|
|
|
2404
2501
|
launchctl unload -w "$(_launchd_plist_path "loop" "$project_path")" 2>/dev/null || true
|
|
2405
2502
|
else
|
|
2406
2503
|
local slug; slug=$(_project_slug "$project_path")
|
|
2504
|
+
mkdir -p "${_SHARED_ROOT}/loop"
|
|
2407
2505
|
touch "${_SHARED_ROOT}/loop/PAUSE-${slug}"
|
|
2408
2506
|
fi
|
|
2409
2507
|
|
|
@@ -2563,11 +2661,12 @@ _loop_runs() {
|
|
|
2563
2661
|
fi
|
|
2564
2662
|
|
|
2565
2663
|
local project_path; project_path=$(pwd -P)
|
|
2664
|
+
local project_slug; project_slug=$(_project_slug "$project_path")
|
|
2566
2665
|
local filtered
|
|
2567
2666
|
if $all_flag; then
|
|
2568
2667
|
filtered=$(cat "$_LOOP_RUNS")
|
|
2569
2668
|
else
|
|
2570
|
-
filtered=$(jq -c --arg p "$
|
|
2669
|
+
filtered=$(jq -c --arg p "$project_slug" 'select(.project == $p)' "$_LOOP_RUNS")
|
|
2571
2670
|
fi
|
|
2572
2671
|
|
|
2573
2672
|
if [[ -z "$filtered" ]]; then
|
|
@@ -2795,6 +2894,35 @@ cmd_brief() {
|
|
|
2795
2894
|
[[ -f "$latest" ]] && cat "$latest"
|
|
2796
2895
|
}
|
|
2797
2896
|
|
|
2897
|
+
_promote_unreleased() {
|
|
2898
|
+
local version="$1"
|
|
2899
|
+
local changelog="$2"
|
|
2900
|
+
[[ -f "$changelog" ]] || return 0
|
|
2901
|
+
grep -q "^## Unreleased" "$changelog" || return 0
|
|
2902
|
+
sed -i.bak "s/^## Unreleased$/## v${version}/" "$changelog" && rm "${changelog}.bak"
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
_ensure_unreleased() {
|
|
2906
|
+
local changelog="$1"
|
|
2907
|
+
[[ -f "$changelog" ]] || return 0
|
|
2908
|
+
grep -q "^## Unreleased$" "$changelog" && return 0
|
|
2909
|
+
python3 - "$changelog" <<'PYEOF'
|
|
2910
|
+
import sys, pathlib
|
|
2911
|
+
p = pathlib.Path(sys.argv[1])
|
|
2912
|
+
lines = p.read_text().splitlines(keepends=True)
|
|
2913
|
+
out = []
|
|
2914
|
+
inserted = False
|
|
2915
|
+
for line in lines:
|
|
2916
|
+
out.append(line)
|
|
2917
|
+
if not inserted and line.rstrip() == "# Changelog":
|
|
2918
|
+
out.append("\n## Unreleased\n")
|
|
2919
|
+
inserted = True
|
|
2920
|
+
if not inserted:
|
|
2921
|
+
out.insert(0, "## Unreleased\n\n")
|
|
2922
|
+
p.write_text("".join(out))
|
|
2923
|
+
PYEOF
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2798
2926
|
cmd_release() {
|
|
2799
2927
|
local pkg; pkg=$(node -p "require('./package.json').name" 2>/dev/null || true)
|
|
2800
2928
|
if [[ "$pkg" == "@seanyao/roll" ]]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.512.
|
|
3
|
+
"version": "2026.512.6",
|
|
4
4
|
"description": "Roll — Roll out features with AI agents",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "find tests/unit tests/integration -name '*.bats' | sort | xargs ./tests/helpers/bats-core/bin/bats"
|
|
@@ -196,7 +196,7 @@ Optional field, only when `status == "failed"`:
|
|
|
196
196
|
|
|
197
197
|
```bash
|
|
198
198
|
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
199
|
-
project=$(
|
|
199
|
+
project=$(_project_slug "$(pwd -P)") # must match roll loop runs filter
|
|
200
200
|
# duration_sec = cycle_end_epoch - cycle_start_epoch (track at Step 1)
|
|
201
201
|
# tcr_count = git log --oneline --since="<cycle_start>" | grep -c '^[a-f0-9]* tcr:'
|
|
202
202
|
|