@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 CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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.2"
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
- case "$to" in
1437
- claude)
1438
- output="$(claude -p --output-format text "$prompt" 2>"$stderr_log" || true)"
1439
- ;;
1440
- kimi)
1441
- output="$(kimi --quiet -p "$prompt" 2>"$stderr_log" || true)"
1442
- ;;
1443
- pi)
1444
- output="$(pi -p "$prompt" 2>"$stderr_log" || true)"
1445
- ;;
1446
- deepseek)
1447
- output="$(deepseek "$prompt" 2>"$stderr_log" || true)"
1448
- ;;
1449
- codex)
1450
- output="$(codex exec --json --output-last-message "$prompt" 2>"$stderr_log" || true)"
1451
- ;;
1452
- opencode)
1453
- output="$(opencode run "$prompt" 2>"$stderr_log" || true)"
1454
- ;;
1455
- *)
1456
- err "Unsupported peer: $to 不支持的 peer: $to"
1457
- return 1
1458
- ;;
1459
- esac
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
- if [ -f "\$FMT" ]; then
1967
- ( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
1968
- else
1969
- cd "${project_path}" && ${cmd_verbose}
1970
- fi
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
- if [ "\$_PREF" = "ghostty" ] || [ "\$_PREF" = "Ghostty" ]; then
2019
- open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
2020
- fi
2021
- if [ "\$_launched" -eq 0 ] && { [ "\$_PREF" = "iTerm2" ] || [ "\$_PREF" = "iTerm" ] || [ -d "/Applications/iTerm.app" ]; }; then
2022
- osascript \\
2023
- -e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
2024
- -e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
2025
- -e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \\
2026
- && _launched=1 || true
2027
- fi
2028
- if [ "\$_launched" -eq 0 ] && [ -d "/Applications/Ghostty.app" ]; then
2029
- open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
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 "$project_path" 'select(.project == $p)' "$_LOOP_RUNS")
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.2",
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=$(basename "$(pwd)" | sed 's/.*-//') # or use _project_slug if available
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