@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 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.5"
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"
@@ -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
- if [ "\$_PREF" = "ghostty" ] || [ "\$_PREF" = "Ghostty" ]; then
2028
- open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
2029
- fi
2030
- if [ "\$_launched" -eq 0 ] && { [ "\$_PREF" = "iTerm2" ] || [ "\$_PREF" = "iTerm" ] || [ -d "/Applications/iTerm.app" ]; }; then
2031
- osascript \\
2032
- -e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
2033
- -e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
2034
- -e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \\
2035
- && _launched=1 || true
2036
- fi
2037
- if [ "\$_launched" -eq 0 ] && [ -d "/Applications/Ghostty.app" ]; then
2038
- open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
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 "$project_path" 'select(.project == $p)' "$_LOOP_RUNS")
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.5",
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