@seanyao/roll 2026.512.5 → 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 +5 -0
- package/bin/roll +160 -41
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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
|
+
|
|
3
8
|
## v2026.512.5
|
|
4
9
|
- **Fixed**: loop 遇到 API 错误时自动重试,不再直接中断
|
|
5
10
|
|
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"
|
|
@@ -2022,21 +2112,19 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
2022
2112
|
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2023
2113
|
# Runtime terminal detection: try preferred first, fallback through installed apps.
|
|
2024
2114
|
# open -na returns non-zero when app not found, so || chain works as fallback.
|
|
2025
|
-
_PREF="${terminal_pref}"
|
|
2026
2115
|
_launched=0
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
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
|
|
2040
2128
|
if [ "\$_launched" -eq 0 ] && command -v osascript >/dev/null 2>&1; then
|
|
2041
2129
|
osascript \\
|
|
2042
2130
|
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
@@ -2413,6 +2501,7 @@ _loop_pause() {
|
|
|
2413
2501
|
launchctl unload -w "$(_launchd_plist_path "loop" "$project_path")" 2>/dev/null || true
|
|
2414
2502
|
else
|
|
2415
2503
|
local slug; slug=$(_project_slug "$project_path")
|
|
2504
|
+
mkdir -p "${_SHARED_ROOT}/loop"
|
|
2416
2505
|
touch "${_SHARED_ROOT}/loop/PAUSE-${slug}"
|
|
2417
2506
|
fi
|
|
2418
2507
|
|
|
@@ -2572,11 +2661,12 @@ _loop_runs() {
|
|
|
2572
2661
|
fi
|
|
2573
2662
|
|
|
2574
2663
|
local project_path; project_path=$(pwd -P)
|
|
2664
|
+
local project_slug; project_slug=$(_project_slug "$project_path")
|
|
2575
2665
|
local filtered
|
|
2576
2666
|
if $all_flag; then
|
|
2577
2667
|
filtered=$(cat "$_LOOP_RUNS")
|
|
2578
2668
|
else
|
|
2579
|
-
filtered=$(jq -c --arg p "$
|
|
2669
|
+
filtered=$(jq -c --arg p "$project_slug" 'select(.project == $p)' "$_LOOP_RUNS")
|
|
2580
2670
|
fi
|
|
2581
2671
|
|
|
2582
2672
|
if [[ -z "$filtered" ]]; then
|
|
@@ -2804,6 +2894,35 @@ cmd_brief() {
|
|
|
2804
2894
|
[[ -f "$latest" ]] && cat "$latest"
|
|
2805
2895
|
}
|
|
2806
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
|
+
|
|
2807
2926
|
cmd_release() {
|
|
2808
2927
|
local pkg; pkg=$(node -p "require('./package.json').name" 2>/dev/null || true)
|
|
2809
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
|
|