@seanyao/roll 2026.514.3 → 2026.515.1

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.515.1
4
+
5
+ - **New**: `roll brief` / `roll dream` 生成文档后自动提交推送 — 每次晨报和夜检不再需要手动 commit `[loop]`
6
+ - **New**: 双语 FAQ 指南 — 10 个自治交付常见场景(loop 卡住、PR 冲突、agent 切换、权限问题等),每条含原因和原理,EN + ZH 对照 `[docs]`
7
+ - **New**: 可选的事件驱动 PR 评审模板 — `cp templates/workflows/pr-review-event.yml .github/workflows/`,PR 开即触发 AI 评审,不装也行(loop 每轮兜底) `[pr]`
8
+ - **New**: loop PR inbox 从"分类但空转"升级到"分类+执行" — eligible PR 自动调 AI 评审,stale PR 自动 rebase,fork 和冲突写 ALERT;bot 已评审的 PR 自动让步 `[loop]`
9
+ - **New**: `roll review-pr <number>` — agent-agnostic AI 代码评审,任意 agent(Claude/Kimi/DeepSeek 等)均可评审任意 git 平台的 PR;PR body 加 `[skip-ai-review]` 可跳过 `[pr]`
10
+ - **Fixed**: `roll peer` 终态后 tmux session 不再残留 — AGREE/ESCALATE/UNKNOWN/round≥3 自动 kill,round<3 保留复用 `[peer]`
11
+ - **New**: `loop/cycle-*` 远程僵尸分支兜底 GC — 每轮 cycle 结尾扫描已合入 main 的 `loop/cycle-*` 分支并删除,弥补 PR auto-merge 失败时的清理盲区 `[loop]`
12
+
13
+ ## v2026.514.5
14
+
15
+ - **Fixed**: 上版 `claude/*` 临时分支清理意外失效 — 现已恢复 `[loop]`
16
+ - **Fixed**: loop session 结束后本地 worktree 不再积累,`git worktree list` 保持干净 `[loop]`
17
+ - **Fixed**: 发版脚本不再维护独立的 agent 检测逻辑,配置变更时两处不再悄悄漂移
18
+
3
19
  ## v2026.514.3
4
20
 
5
21
  ### 约定与导航
package/README.md CHANGED
@@ -62,7 +62,9 @@ roll loop on # optional: let the agent work unattended
62
62
  | Loop (autonomous executor) | [guide/en/loop.md](docs/guide/en/loop.md) | [guide/zh/loop.md](docs/guide/zh/loop.md) |
63
63
  | Dream (nightly health scan) | [guide/en/dream.md](docs/guide/en/dream.md) | [guide/zh/dream.md](docs/guide/zh/dream.md) |
64
64
  | Peer (cross-agent review) | [guide/en/peer.md](docs/guide/en/peer.md) | [guide/zh/peer.md](docs/guide/zh/peer.md) |
65
+ | Configuration (env vars) | [guide/en/configuration.md](docs/guide/en/configuration.md) | [guide/zh/configuration.md](docs/guide/zh/configuration.md) |
65
66
  | Skill selection guide | [guide/en/skills.md](docs/guide/en/skills.md) | [guide/zh/skills.md](docs/guide/zh/skills.md) |
67
+ | FAQ (troubleshooting) | [guide/en/faq.md](docs/guide/en/faq.md) | [guide/zh/faq.md](docs/guide/zh/faq.md) |
66
68
  | Domain model (DDD) | [domain/context-map.md](docs/domain/context-map.md) | — |
67
69
  | Engineering common sense | [practices/engineering-common-sense.md](docs/practices/engineering-common-sense.md) | — |
68
70
 
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.514.3"
7
+ VERSION="2026.515.1"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -465,7 +465,9 @@ _link_skills() {
465
465
  local current_target
466
466
  current_target="$(readlink "$skill_link")"
467
467
  if [[ "$current_target" != "$skill_dir" ]]; then
468
- ln -sf "$skill_dir" "$skill_link"
468
+ # macOS ln -sf follows symlinks-to-dirs and creates inside instead of
469
+ # replacing — explicitly remove first to guarantee replacement.
470
+ rm -f "$skill_link" && ln -s "$skill_dir" "$skill_link"
469
471
  repaired=$((repaired + 1))
470
472
  fi
471
473
  # correct symlink: skip silently
@@ -612,6 +614,7 @@ cmd_setup() {
612
614
 
613
615
  echo ""
614
616
  _print_pr_pipeline_hint
617
+ _print_pr_event_hint
615
618
  }
616
619
 
617
620
  # ─── PR pipeline hint ────────────────────────────────────────────────────────
@@ -641,6 +644,24 @@ _print_pr_pipeline_hint() {
641
644
  HINT
642
645
  }
643
646
 
647
+ _print_pr_event_hint() {
648
+ cat <<'HINT'
649
+
650
+ Optional — enable event-driven PR review (seconds-fast, GitHub only).
651
+ 可选 —— 启用事件驱动 PR 评审(秒级响应,仅限 GitHub)。
652
+
653
+ Without this, Roll reviews PRs each loop cycle (~1h). With it,
654
+ contributors get AI feedback on PR open/update immediately.
655
+ 不安装也行 — loop 每轮会兜底评审。安装后 PR 一开即触发 AI 评审。
656
+
657
+ cp templates/workflows/pr-review-event.yml .github/workflows/
658
+
659
+ Then set the API key secret for your configured agent in GitHub repo settings.
660
+ 然后在 GitHub 仓库设置中添加你配置的 agent 对应的 API key secret。
661
+
662
+ HINT
663
+ }
664
+
644
665
  # ═══════════════════════════════════════════════════════════════════════════════
645
666
  # COMMAND: update
646
667
  # Thin wrapper: upgrade the npm-installed package, then re-sync via setup.
@@ -1775,6 +1796,16 @@ cmd_peer() {
1775
1796
  echo ""
1776
1797
  info "Log: $log_file"
1777
1798
 
1799
+ local _should_kill=true
1800
+ case "$resolution" in
1801
+ REFINE|OBJECT) [[ "$round" -lt 3 ]] && _should_kill=false ;;
1802
+ esac
1803
+ if [[ "$_should_kill" == "true" ]] && [[ -n "$peer_session" ]] \
1804
+ && command -v tmux >/dev/null 2>&1 \
1805
+ && tmux has-session -t "$peer_session" 2>/dev/null; then
1806
+ tmux kill-session -t "$peer_session" 2>/dev/null || true
1807
+ fi
1808
+
1778
1809
  case "$resolution" in
1779
1810
  AGREE) exit 0 ;;
1780
1811
  REFINE|OBJECT) exit 2 ;;
@@ -1896,6 +1927,17 @@ _skill_content() {
1896
1927
  awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' "$1"
1897
1928
  }
1898
1929
 
1930
+ _parse_review_verdict() {
1931
+ local output="$1"
1932
+ local line
1933
+ line=$(echo "$output" | grep -o '<!--VERDICT:[^>]*-->' | tail -1)
1934
+ [ -n "$line" ] || return 0
1935
+ local type reason
1936
+ type=$(echo "$line" | sed -E 's/<!--VERDICT:([A-Z_]+)(:.*)?-->/\1/')
1937
+ reason=$(echo "$line" | sed -E 's/<!--VERDICT:[A-Z_]+:?//; s/-->//' | sed 's/^ *//')
1938
+ echo "${type}${reason:+:${reason}}"
1939
+ }
1940
+
1899
1941
  _agent_run_skill() {
1900
1942
  local skill="$1"
1901
1943
  local agent; agent=$(_project_agent)
@@ -1913,6 +1955,102 @@ _agent_run_skill() {
1913
1955
  esac
1914
1956
  }
1915
1957
 
1958
+ cmd_review_pr() {
1959
+ local pr_number="${1:-}"
1960
+ [ -n "$pr_number" ] || { err "Usage: roll review-pr <number>"; return 1; }
1961
+
1962
+ local slug; slug=$(_gh_repo_slug) || { err "Not a GitHub repo — review-pr requires GitHub remote"; return 1; }
1963
+
1964
+ local pr_json
1965
+ pr_json=$(gh -R "$slug" pr view "$pr_number" --json title,body,diff 2>&1) \
1966
+ || { err "gh pr view failed: ${pr_json}"; return 1; }
1967
+
1968
+ local title body diff
1969
+ title=$(echo "$pr_json" | jq -r '.title // ""')
1970
+ body=$(echo "$pr_json" | jq -r '.body // ""')
1971
+ diff=$(echo "$pr_json" | jq -r '.diff // ""')
1972
+
1973
+ if echo "$body" | grep -qF '[skip-ai-review]'; then
1974
+ gh -R "$slug" pr review "$pr_number" --approve -b "Auto-approved: [skip-ai-review] detected" 2>/dev/null || true
1975
+ info "PR #${pr_number}: [skip-ai-review] — auto-approved"
1976
+ return 0
1977
+ fi
1978
+
1979
+ local template="${ROLL_PKG_DIR}/skills/roll-review-pr/SKILL.md"
1980
+ [ -f "$template" ] || { err "Skill template not found: ${template}"; return 1; }
1981
+
1982
+ local content; content=$(_skill_content "$template")
1983
+
1984
+ local tmp; tmp=$(mktemp)
1985
+ # shellcheck disable=SC2064
1986
+ trap "rm -f '$tmp'" EXIT
1987
+
1988
+ echo "$content" > "$tmp"
1989
+ sed -i '' "s|{{PR_TITLE}}|${title}|g" "$tmp" 2>/dev/null \
1990
+ || sed -i "s|{{PR_TITLE}}|${title}|g" "$tmp"
1991
+
1992
+ local body_escaped; body_escaped=$(printf '%s' "$body" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')
1993
+ sed -i '' "s|{{PR_BODY}}|${body_escaped}|g" "$tmp" 2>/dev/null \
1994
+ || sed -i "s|{{PR_BODY}}|${body_escaped}|g" "$tmp"
1995
+
1996
+ local diff_truncated
1997
+ diff_truncated=$(echo "$diff" | head -500)
1998
+ local diff_file; diff_file=$(mktemp)
1999
+ echo "$diff_truncated" > "$diff_file"
2000
+ awk -v f="$diff_file" '{
2001
+ if (index($0, "{{PR_DIFF}}")) {
2002
+ while ((getline line < f) > 0) print line
2003
+ close(f)
2004
+ } else print
2005
+ }' "$tmp" > "${tmp}.out" && mv "${tmp}.out" "$tmp"
2006
+ rm -f "$diff_file"
2007
+
2008
+ local prompt; prompt=$(cat "$tmp")
2009
+ rm -f "$tmp"
2010
+ trap - EXIT
2011
+
2012
+ local agent; agent=$(_project_agent)
2013
+ local output
2014
+ info "Reviewing PR #${pr_number} with ${agent}..."
2015
+ case "$agent" in
2016
+ claude) output=$(claude -p --output-format text "$prompt" 2>/dev/null) ;;
2017
+ kimi) output=$(kimi --quiet -p "$prompt" 2>/dev/null) ;;
2018
+ deepseek) output=$(deepseek "$prompt" 2>/dev/null) ;;
2019
+ pi) output=$(pi -p "$prompt" 2>/dev/null) ;;
2020
+ codex) output=$(codex exec "$prompt" 2>/dev/null) ;;
2021
+ opencode) output=$(opencode run "$prompt" 2>/dev/null) ;;
2022
+ *) err "Unknown agent '${agent}'"; return 1 ;;
2023
+ esac
2024
+
2025
+ echo "$output"
2026
+
2027
+ local verdict; verdict=$(_parse_review_verdict "$output")
2028
+ local vtype; vtype="${verdict%%:*}"
2029
+ local vreason; vreason="${verdict#*:}"
2030
+ [ "$vreason" = "$vtype" ] && vreason=""
2031
+
2032
+ case "$vtype" in
2033
+ APPROVE)
2034
+ gh -R "$slug" pr review "$pr_number" --approve -b "AI review: approved" 2>/dev/null || true
2035
+ info "PR #${pr_number}: APPROVED"
2036
+ ;;
2037
+ REQUEST_CHANGES)
2038
+ gh -R "$slug" pr review "$pr_number" --request-changes -b "${vreason:-AI review requested changes}" 2>/dev/null || true
2039
+ info "PR #${pr_number}: REQUEST_CHANGES — ${vreason}"
2040
+ ;;
2041
+ UNCERTAIN)
2042
+ warn "PR #${pr_number}: UNCERTAIN — ${vreason}"
2043
+ local alert_file="${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/ALERT.md"
2044
+ mkdir -p "$(dirname "$alert_file")"
2045
+ printf '[%s] PR #%s: AI review UNCERTAIN — %s\n' \
2046
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr_number" "$vreason" >> "$alert_file"
2047
+ ;;
2048
+ *)
2049
+ warn "PR #${pr_number}: no verdict parsed from agent output"
2050
+ ;;
2051
+ esac
2052
+ }
2053
+
1916
2054
  cmd_agent() {
1917
2055
  local subcmd="${1:-}"; shift || true
1918
2056
  case "$subcmd" in
@@ -1927,8 +2065,12 @@ cmd_agent() {
1927
2065
  fi
1928
2066
  ok "Agent set to ${name} for this project 当前项目 agent 已设为 ${name}"
1929
2067
  local project_path; project_path=$(pwd -P)
1930
- crontab -l 2>/dev/null | grep -q "roll-loop:${project_path}" && \
1931
- info "Takes effect on next scheduled run — or: roll loop now"
2068
+ local slug; slug=$(_project_slug "$project_path")
2069
+ local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
2070
+ if [[ -f "$runner" ]]; then
2071
+ _install_launchd_plists "$project_path" >/dev/null
2072
+ ok "Loop runner scripts regenerated for new agent 已为新 agent 重新生成 loop 脚本"
2073
+ fi
1932
2074
  ;;
1933
2075
  list)
1934
2076
  echo ""; echo " Available agents 可用 agent:"; echo ""
@@ -2123,6 +2265,9 @@ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
2123
2265
  BRANCH="loop/cycle-\${CYCLE_ID}"
2124
2266
  _USE_WORKTREE=0
2125
2267
  cd "${project_path}" 2>/dev/null || true
2268
+ # US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
2269
+ # post-claude cleanup can diff and delete only this session's additions.
2270
+ CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
2126
2271
  if _worktree_fetch_origin main \\
2127
2272
  && _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
2128
2273
  _USE_WORKTREE=1
@@ -2148,6 +2293,14 @@ for _attempt in 1 2 3; do
2148
2293
  fi
2149
2294
  done
2150
2295
 
2296
+ # US-AUTO-038: diff snapshot vs current and delete any claude/* branches this
2297
+ # session pushed to origin. Runs regardless of claude's exit code (cleanup is
2298
+ # orthogonal to success/failure) and is silent on non-GitHub / unreachable.
2299
+ _claude_cleanup_new_branches "\$CLAUDE_BRANCH_SNAPSHOT" "${project_path}" || true
2300
+ # REFACTOR-011: also prune local .claude/worktrees/ entries whose branch has
2301
+ # been merged to main (remote-branch cleanup above doesn't touch local worktrees).
2302
+ _claude_cleanup_stale_worktrees "${project_path}" || true
2303
+
2151
2304
  # Post-claude: publish cycle branch. Doc-only changes (BACKLOG/docs) merge
2152
2305
  # immediately via --admin; code changes use auto-merge (CI gate required).
2153
2306
  # When \`gh\` is unavailable, fall back to the legacy ff-merge path.
@@ -2179,6 +2332,9 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
2179
2332
  echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
2180
2333
  fi
2181
2334
  fi
2335
+
2336
+ # US-AUTO-040: fallback GC — delete remote loop/cycle-* branches already merged to main.
2337
+ _loop_cleanup_stale_cycle_branches "${project_path}" || true
2182
2338
  INNER
2183
2339
  chmod +x "$inner_path"
2184
2340
 
@@ -2214,7 +2370,9 @@ fi
2214
2370
  echo "\$\$" > "\$LOCK"
2215
2371
  trap 'rm -f "\$LOCK"' EXIT
2216
2372
  if command -v tmux >/dev/null 2>&1; then
2217
- tmux kill-session -t "\$SESSION" 2>/dev/null || true
2373
+ tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-" | while read _s; do
2374
+ tmux kill-session -t "\$_s" 2>/dev/null || true
2375
+ done
2218
2376
  tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
2219
2377
  tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
2220
2378
  # Auto-attach popup: when not muted, spawn a Terminal window attached to the
@@ -3306,6 +3464,55 @@ _loop_pr_state_write() {
3306
3464
  ' "$state" > "$tmp" && mv "$tmp" "$state"
3307
3465
  }
3308
3466
 
3467
+ # _loop_pr_review_external <pr_number>
3468
+ # Calls cmd_review_pr (US-PR-001) to run AI review on an eligible external PR.
3469
+ # Lenient: errors are logged but do not fail the loop.
3470
+ _loop_pr_review_external() {
3471
+ local pr="$1"
3472
+ [ -n "$pr" ] || return 0
3473
+ cmd_review_pr "$pr" 2>&1 || {
3474
+ warn "review-pr failed for PR #${pr} (non-fatal)"
3475
+ return 0
3476
+ }
3477
+ }
3478
+
3479
+ # _loop_pr_rebase_stale <pr_number> <head_ref>
3480
+ # Attempts to rebase a stale PR onto origin/main and push.
3481
+ # Fork PRs are skipped (no write access). Conflicts write ALERT.
3482
+ _loop_pr_rebase_stale() {
3483
+ local pr="$1" head_ref="$2"
3484
+ [ -n "$pr" ] && [ -n "$head_ref" ] || return 0
3485
+
3486
+ local slug; slug=$(_gh_repo_slug 2>/dev/null) || return 0
3487
+
3488
+ local pr_json
3489
+ pr_json=$(gh -R "$slug" pr view "$pr" --json headRepository,headRepositoryOwner,isCrossRepository 2>/dev/null) || return 0
3490
+ local is_fork
3491
+ is_fork=$(echo "$pr_json" | jq -r '.isCrossRepository // false' 2>/dev/null)
3492
+ if [ "$is_fork" = "true" ]; then
3493
+ local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
3494
+ mkdir -p "$(dirname "$alert")" 2>/dev/null || true
3495
+ printf '[%s] PR #%s: fork PR — cannot rebase (no write access)\n' \
3496
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" >> "$alert"
3497
+ return 0
3498
+ fi
3499
+
3500
+ git fetch origin "$head_ref" 2>/dev/null || return 0
3501
+ if git checkout "$head_ref" 2>/dev/null \
3502
+ && git rebase origin/main 2>/dev/null \
3503
+ && git push origin "$head_ref" 2>/dev/null; then
3504
+ info "PR #${pr}: rebased ${head_ref} onto origin/main"
3505
+ else
3506
+ git rebase --abort 2>/dev/null || true
3507
+ git checkout - 2>/dev/null || true
3508
+ local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
3509
+ mkdir -p "$(dirname "$alert")" 2>/dev/null || true
3510
+ printf '[%s] PR #%s: rebase conflict on %s — please rebase manually\n' \
3511
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" "$head_ref" >> "$alert"
3512
+ fi
3513
+ return 0
3514
+ }
3515
+
3309
3516
  # _loop_pr_inbox
3310
3517
  # Walks open PRs and routes each by classification.
3311
3518
  # Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
@@ -3335,10 +3542,13 @@ _loop_pr_inbox() {
3335
3542
  --json reviews,mergeStateStatus,statusCheckRollup \
3336
3543
  2>/dev/null) || { i=$((i + 1)); continue; }
3337
3544
 
3338
- local human_review ci_state mergeable
3545
+ local human_review ci_state mergeable bot_review
3339
3546
  human_review=$(echo "$view_json" | jq -r '
3340
3547
  [.reviews[]? | select(.authorAssociation != "BOT" and .authorAssociation != "APP")]
3341
3548
  | last // {} | .state // ""' 2>/dev/null)
3549
+ bot_review=$(echo "$view_json" | jq -r '
3550
+ [.reviews[]? | select(.authorAssociation == "BOT" or .authorAssociation == "APP")]
3551
+ | last // {} | .state // ""' 2>/dev/null)
3342
3552
  mergeable=$(echo "$view_json" | jq -r '.mergeStateStatus // ""' 2>/dev/null)
3343
3553
  ci_state=$(echo "$view_json" | jq -r '
3344
3554
  if (.statusCheckRollup | length) == 0 then ""
@@ -3346,6 +3556,17 @@ _loop_pr_inbox() {
3346
3556
  elif all(.statusCheckRollup[]?; .conclusion == "SUCCESS" or .conclusion == "SKIPPED") then "success"
3347
3557
  else "pending" end' 2>/dev/null)
3348
3558
 
3559
+ # Bot review gate: if a GHA workflow already handled this PR, defer to it.
3560
+ if [ "$bot_review" = "APPROVED" ]; then
3561
+ i=$((i + 1)); continue
3562
+ elif [ "$bot_review" = "CHANGES_REQUESTED" ]; then
3563
+ local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
3564
+ mkdir -p "$(dirname "$alert")" 2>/dev/null || true
3565
+ printf '[%s] PR #%s: bot review CHANGES_REQUESTED — loop PR rejected by GHA reviewer\n' \
3566
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$num" >> "$alert"
3567
+ i=$((i + 1)); continue
3568
+ fi
3569
+
3349
3570
  local verdict
3350
3571
  verdict=$(_loop_pr_classify "$head_ref" "$human_review" "$ci_state" "$mergeable")
3351
3572
 
@@ -3355,15 +3576,10 @@ _loop_pr_inbox() {
3355
3576
  ;;
3356
3577
  stale)
3357
3578
  _loop_pr_rebase_circuit "$num" || true
3358
- # Actual rebase work delegated to _loop_pr_rebase_stale when present
3359
- # (kept out of this cycle to avoid touching git remote in tests).
3360
- command -v _loop_pr_rebase_stale >/dev/null 2>&1 \
3361
- && _loop_pr_rebase_stale "$num" "$head_ref" || true
3579
+ _loop_pr_rebase_stale "$num" "$head_ref" || true
3362
3580
  ;;
3363
3581
  eligible)
3364
- # Hand off to review (US-AUTO-035 supplies the actual decision).
3365
- command -v _loop_pr_review_external >/dev/null 2>&1 \
3366
- && _loop_pr_review_external "$num" || true
3582
+ _loop_pr_review_external "$num" || true
3367
3583
  ;;
3368
3584
  esac
3369
3585
 
@@ -3633,6 +3849,97 @@ _worktree_merge_back() {
3633
3849
  return 0
3634
3850
  }
3635
3851
 
3852
+ # _claude_remote_snapshot [repo]
3853
+ # Echo the current set of remote `claude/*` branch names (sans
3854
+ # refs/heads/), one per line, sorted. Silent on remote unreachable / no
3855
+ # remote / no matches — empty stdout, exit 0.
3856
+ _claude_remote_snapshot() {
3857
+ local repo="${1:-.}"
3858
+ git -C "$repo" ls-remote --heads origin 'refs/heads/claude/*' 2>/dev/null \
3859
+ | awk '{print $2}' \
3860
+ | sed 's|^refs/heads/||' \
3861
+ | sort
3862
+ }
3863
+
3864
+ # _claude_cleanup_new_branches <prior> [repo]
3865
+ # Delete remote `claude/*` branches present now but absent from <prior>
3866
+ # (newline-separated list, as emitted by _claude_remote_snapshot). Skips
3867
+ # silently when origin is not a GitHub remote. Each successful delete logs
3868
+ # one INFO line; failures are silently ignored so the loop's main flow is
3869
+ # never derailed.
3870
+ _claude_cleanup_new_branches() {
3871
+ local prior="$1"
3872
+ local repo="${2:-.}"
3873
+ local url; url=$(git -C "$repo" remote get-url origin 2>/dev/null)
3874
+ [[ "$url" == *github.com* ]] || return 0
3875
+ local current; current=$(_claude_remote_snapshot "$repo")
3876
+ [ -z "$current" ] && return 0
3877
+ local prior_sorted; prior_sorted=$(printf '%s\n' "$prior" | sort -u)
3878
+ local new_branches
3879
+ new_branches=$(comm -13 <(printf '%s\n' "$prior_sorted") <(printf '%s\n' "$current"))
3880
+ [ -z "$new_branches" ] && return 0
3881
+ while IFS= read -r branch; do
3882
+ [ -z "$branch" ] && continue
3883
+ if git -C "$repo" push origin --delete "$branch" 2>/dev/null; then
3884
+ echo "[loop] deleted stale claude branch: $branch"
3885
+ fi
3886
+ done <<< "$new_branches"
3887
+ return 0
3888
+ }
3889
+
3890
+ # _claude_cleanup_stale_worktrees [project_path]
3891
+ # Remove local worktrees under <project_path>/.claude/worktrees/ whose
3892
+ # branch has been fully merged into main (merge-base --is-ancestor). Active
3893
+ # worktrees (branch ahead of main) are preserved. Runs `git worktree prune`
3894
+ # afterwards to clear stale metadata. Silent on missing directory or any
3895
+ # individual failure so the loop's main flow is never derailed.
3896
+ _claude_cleanup_stale_worktrees() {
3897
+ local project_path="${1:-.}"
3898
+ local wt_dir="${project_path}/.claude/worktrees"
3899
+ [ -d "$wt_dir" ] || return 0
3900
+ local entry branch
3901
+ for entry in "$wt_dir"/*/; do
3902
+ [ -d "$entry" ] || continue
3903
+ branch=$(git -C "$project_path" worktree list --porcelain 2>/dev/null \
3904
+ | awk -v p="${entry%/}" '
3905
+ /^worktree / { cur=$2; flag=(cur==p) }
3906
+ /^branch / && flag { sub(/^refs\/heads\//, "", $2); print $2; flag=0 }
3907
+ ')
3908
+ [ -z "$branch" ] && branch=$(git -C "$entry" symbolic-ref --short HEAD 2>/dev/null)
3909
+ [ -z "$branch" ] && continue
3910
+ if git -C "$project_path" merge-base --is-ancestor "$branch" main 2>/dev/null; then
3911
+ git -C "$project_path" worktree remove --force "$entry" 2>/dev/null || true
3912
+ rm -rf "$entry" 2>/dev/null || true
3913
+ git -C "$project_path" branch -D "$branch" 2>/dev/null || true
3914
+ echo "[loop] removed stale worktree: $branch"
3915
+ fi
3916
+ done
3917
+ git -C "$project_path" worktree prune 2>/dev/null || true
3918
+ return 0
3919
+ }
3920
+
3921
+ _loop_cleanup_stale_cycle_branches() {
3922
+ local project_path="${1:-.}"
3923
+ local url; url=$(git -C "$project_path" remote get-url origin 2>/dev/null) || return 0
3924
+ [[ "$url" == *github.com* ]] || return 0
3925
+
3926
+ local branches
3927
+ branches=$(git -C "$project_path" ls-remote --heads origin 'refs/heads/loop/cycle-*' 2>/dev/null \
3928
+ | awk '{print $2}' | sed 's|^refs/heads/||')
3929
+ [ -z "$branches" ] && return 0
3930
+
3931
+ while IFS= read -r branch; do
3932
+ [ -z "$branch" ] && continue
3933
+ if ! git -C "$project_path" merge-base --is-ancestor "$branch" origin/main 2>/dev/null; then
3934
+ continue
3935
+ fi
3936
+ if git -C "$project_path" push origin --delete "$branch" 2>/dev/null; then
3937
+ echo "[loop] deleted stale cycle branch: $branch"
3938
+ fi
3939
+ done <<< "$branches"
3940
+ return 0
3941
+ }
3942
+
3636
3943
  # US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
3637
3944
  #
3638
3945
  # _loop_publish_pr <branch> [title]
@@ -4637,6 +4944,7 @@ usage() {
4637
4944
  echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
4638
4945
  echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
4639
4946
  echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
4947
+ echo " review-pr <number> [PR Review] AI-powered code review for a PR AI 代码评审"
4640
4948
  echo ""
4641
4949
  echo "Examples / 示例:"
4642
4950
  echo " roll setup # New machine: first-time install 新机器首次安装"
@@ -4668,6 +4976,7 @@ main() {
4668
4976
  agent) cmd_agent "$@" ;;
4669
4977
  release) cmd_release "$@" ;;
4670
4978
  ci) cmd_ci "$@" ;;
4979
+ review-pr) cmd_review_pr "$@" ;;
4671
4980
  version|--version|-v) echo "roll v${VERSION}" ;;
4672
4981
  help|--help|-h) usage ;;
4673
4982
  "") [[ -f "BACKLOG.md" ]] && _dashboard || { usage; _show_changelog; } ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.514.3",
3
+ "version": "2026.515.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -275,6 +275,7 @@ git add BACKLOG.md docs/dream/YYYY-MM-DD.md
275
275
  git commit -m "chore: dream scan YYYY-MM-DD — {N} REFACTOR entries"
276
276
  # 无发现时:
277
277
  git commit -m "chore: dream scan YYYY-MM-DD — no findings"
278
+ git push origin main
278
279
  ```
279
280
 
280
281
  - BACKLOG.md 和 dream 日志必须在**同一个 commit** 里入库,避免出现"REFACTOR 已加但日志找不到"或反过来的撕裂状态
@@ -167,6 +167,7 @@ A simple heuristic — not a gate, just a signal for the human:
167
167
  ```bash
168
168
  git add docs/briefs/YYYY-MM-DD-NN.md
169
169
  git commit -m "docs: roll-brief YYYY-MM-DD-NN — {触发原因}"
170
+ git push origin main
170
171
  ```
171
172
 
172
173
  - 触发原因来自调用上下文(Feature 完成 / 每日 / 手动 / `--feature` / `--since`),用一句话填入
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: roll-review-pr
3
+ license: MIT
4
+ allowed-tools: "Read"
5
+ description: "Agent-agnostic PR review skill. Reviews a pull request diff and emits a structured 3-state verdict (APPROVE / REQUEST_CHANGES / UNCERTAIN). Used by `roll review-pr` and `_loop_pr_review_external`."
6
+ ---
7
+
8
+ # PR Review
9
+
10
+ > Follows the Architecture Constraints, Development Discipline, and Engineering
11
+ > Common Sense defined in the project AGENTS.md.
12
+
13
+ You are reviewing a pull request. Your job is to assess code quality,
14
+ correctness, and adherence to project conventions.
15
+
16
+ ## Context
17
+
18
+ **PR Title:** {{PR_TITLE}}
19
+
20
+ **PR Body:**
21
+ {{PR_BODY}}
22
+
23
+ ## Diff
24
+
25
+ ```diff
26
+ {{PR_DIFF}}
27
+ ```
28
+
29
+ ## Review Instructions
30
+
31
+ 1. Read the diff carefully. Focus on:
32
+ - Correctness: logic errors, off-by-one, unhandled edge cases
33
+ - Security: injection, secrets exposure, unsafe operations
34
+ - Conventions: naming, structure, test coverage (as described in AGENTS.md)
35
+ - Scope: changes should match what the PR title/body claims
36
+
37
+ 2. Write your analysis in free text (2-10 sentences). Be specific — cite file
38
+ names and line numbers when pointing out issues.
39
+
40
+ 3. End your response with exactly ONE verdict footer on its own line:
41
+
42
+ - If the code is acceptable:
43
+ `<!--VERDICT:APPROVE-->`
44
+
45
+ - If changes are needed (cite the most important issue):
46
+ `<!--VERDICT:REQUEST_CHANGES:one-line reason-->`
47
+
48
+ - If you cannot confidently judge (e.g., missing context, domain-specific logic):
49
+ `<!--VERDICT:UNCERTAIN:one-line reason-->`
50
+
51
+ ## Rules
52
+
53
+ - The verdict footer MUST appear on the last non-empty line of your response.
54
+ - Choose exactly one verdict. Do not combine them.
55
+ - REQUEST_CHANGES is for real issues — not style nitpicks or personal preferences.
56
+ - When in doubt between APPROVE and UNCERTAIN, prefer UNCERTAIN.
57
+ - If the PR body contains `[skip-ai-review]`, immediately output
58
+ `<!--VERDICT:APPROVE-->` with no analysis.