@seanyao/roll 2026.514.5 → 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 +10 -0
- package/README.md +1 -0
- package/bin/roll +239 -12
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +1 -0
- package/skills/roll-brief/SKILL.md +1 -0
- package/skills/roll-review-pr/SKILL.md +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## v2026.514.5
|
|
4
14
|
|
|
5
15
|
- **Fixed**: 上版 `claude/*` 临时分支清理意外失效 — 现已恢复 `[loop]`
|
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ roll loop on # optional: let the agent work unattended
|
|
|
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
65
|
| Configuration (env vars) | [guide/en/configuration.md](docs/guide/en/configuration.md) | [guide/zh/configuration.md](docs/guide/zh/configuration.md) |
|
|
66
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) |
|
|
67
68
|
| Domain model (DDD) | [domain/context-map.md](docs/domain/context-map.md) | — |
|
|
68
69
|
| Engineering common sense | [practices/engineering-common-sense.md](docs/practices/engineering-common-sense.md) | — |
|
|
69
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.
|
|
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"
|
|
@@ -614,6 +614,7 @@ cmd_setup() {
|
|
|
614
614
|
|
|
615
615
|
echo ""
|
|
616
616
|
_print_pr_pipeline_hint
|
|
617
|
+
_print_pr_event_hint
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
# ─── PR pipeline hint ────────────────────────────────────────────────────────
|
|
@@ -643,6 +644,24 @@ _print_pr_pipeline_hint() {
|
|
|
643
644
|
HINT
|
|
644
645
|
}
|
|
645
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
|
+
|
|
646
665
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
647
666
|
# COMMAND: update
|
|
648
667
|
# Thin wrapper: upgrade the npm-installed package, then re-sync via setup.
|
|
@@ -1777,6 +1796,16 @@ cmd_peer() {
|
|
|
1777
1796
|
echo ""
|
|
1778
1797
|
info "Log: $log_file"
|
|
1779
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
|
+
|
|
1780
1809
|
case "$resolution" in
|
|
1781
1810
|
AGREE) exit 0 ;;
|
|
1782
1811
|
REFINE|OBJECT) exit 2 ;;
|
|
@@ -1898,6 +1927,17 @@ _skill_content() {
|
|
|
1898
1927
|
awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' "$1"
|
|
1899
1928
|
}
|
|
1900
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
|
+
|
|
1901
1941
|
_agent_run_skill() {
|
|
1902
1942
|
local skill="$1"
|
|
1903
1943
|
local agent; agent=$(_project_agent)
|
|
@@ -1915,6 +1955,102 @@ _agent_run_skill() {
|
|
|
1915
1955
|
esac
|
|
1916
1956
|
}
|
|
1917
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
|
+
|
|
1918
2054
|
cmd_agent() {
|
|
1919
2055
|
local subcmd="${1:-}"; shift || true
|
|
1920
2056
|
case "$subcmd" in
|
|
@@ -1929,8 +2065,12 @@ cmd_agent() {
|
|
|
1929
2065
|
fi
|
|
1930
2066
|
ok "Agent set to ${name} for this project 当前项目 agent 已设为 ${name}"
|
|
1931
2067
|
local project_path; project_path=$(pwd -P)
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
|
1934
2074
|
;;
|
|
1935
2075
|
list)
|
|
1936
2076
|
echo ""; echo " Available agents 可用 agent:"; echo ""
|
|
@@ -2192,6 +2332,9 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2192
2332
|
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2193
2333
|
fi
|
|
2194
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
|
|
2195
2338
|
INNER
|
|
2196
2339
|
chmod +x "$inner_path"
|
|
2197
2340
|
|
|
@@ -2227,7 +2370,9 @@ fi
|
|
|
2227
2370
|
echo "\$\$" > "\$LOCK"
|
|
2228
2371
|
trap 'rm -f "\$LOCK"' EXIT
|
|
2229
2372
|
if command -v tmux >/dev/null 2>&1; then
|
|
2230
|
-
tmux
|
|
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
|
|
2231
2376
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
2232
2377
|
tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
|
|
2233
2378
|
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
@@ -3319,6 +3464,55 @@ _loop_pr_state_write() {
|
|
|
3319
3464
|
' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
3320
3465
|
}
|
|
3321
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
|
+
|
|
3322
3516
|
# _loop_pr_inbox
|
|
3323
3517
|
# Walks open PRs and routes each by classification.
|
|
3324
3518
|
# Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
|
|
@@ -3348,10 +3542,13 @@ _loop_pr_inbox() {
|
|
|
3348
3542
|
--json reviews,mergeStateStatus,statusCheckRollup \
|
|
3349
3543
|
2>/dev/null) || { i=$((i + 1)); continue; }
|
|
3350
3544
|
|
|
3351
|
-
local human_review ci_state mergeable
|
|
3545
|
+
local human_review ci_state mergeable bot_review
|
|
3352
3546
|
human_review=$(echo "$view_json" | jq -r '
|
|
3353
3547
|
[.reviews[]? | select(.authorAssociation != "BOT" and .authorAssociation != "APP")]
|
|
3354
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)
|
|
3355
3552
|
mergeable=$(echo "$view_json" | jq -r '.mergeStateStatus // ""' 2>/dev/null)
|
|
3356
3553
|
ci_state=$(echo "$view_json" | jq -r '
|
|
3357
3554
|
if (.statusCheckRollup | length) == 0 then ""
|
|
@@ -3359,6 +3556,17 @@ _loop_pr_inbox() {
|
|
|
3359
3556
|
elif all(.statusCheckRollup[]?; .conclusion == "SUCCESS" or .conclusion == "SKIPPED") then "success"
|
|
3360
3557
|
else "pending" end' 2>/dev/null)
|
|
3361
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
|
+
|
|
3362
3570
|
local verdict
|
|
3363
3571
|
verdict=$(_loop_pr_classify "$head_ref" "$human_review" "$ci_state" "$mergeable")
|
|
3364
3572
|
|
|
@@ -3368,15 +3576,10 @@ _loop_pr_inbox() {
|
|
|
3368
3576
|
;;
|
|
3369
3577
|
stale)
|
|
3370
3578
|
_loop_pr_rebase_circuit "$num" || true
|
|
3371
|
-
|
|
3372
|
-
# (kept out of this cycle to avoid touching git remote in tests).
|
|
3373
|
-
command -v _loop_pr_rebase_stale >/dev/null 2>&1 \
|
|
3374
|
-
&& _loop_pr_rebase_stale "$num" "$head_ref" || true
|
|
3579
|
+
_loop_pr_rebase_stale "$num" "$head_ref" || true
|
|
3375
3580
|
;;
|
|
3376
3581
|
eligible)
|
|
3377
|
-
|
|
3378
|
-
command -v _loop_pr_review_external >/dev/null 2>&1 \
|
|
3379
|
-
&& _loop_pr_review_external "$num" || true
|
|
3582
|
+
_loop_pr_review_external "$num" || true
|
|
3380
3583
|
;;
|
|
3381
3584
|
esac
|
|
3382
3585
|
|
|
@@ -3715,6 +3918,28 @@ _claude_cleanup_stale_worktrees() {
|
|
|
3715
3918
|
return 0
|
|
3716
3919
|
}
|
|
3717
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
|
+
|
|
3718
3943
|
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
3719
3944
|
#
|
|
3720
3945
|
# _loop_publish_pr <branch> [title]
|
|
@@ -4719,6 +4944,7 @@ usage() {
|
|
|
4719
4944
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
4720
4945
|
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
4721
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 代码评审"
|
|
4722
4948
|
echo ""
|
|
4723
4949
|
echo "Examples / 示例:"
|
|
4724
4950
|
echo " roll setup # New machine: first-time install 新机器首次安装"
|
|
@@ -4750,6 +4976,7 @@ main() {
|
|
|
4750
4976
|
agent) cmd_agent "$@" ;;
|
|
4751
4977
|
release) cmd_release "$@" ;;
|
|
4752
4978
|
ci) cmd_ci "$@" ;;
|
|
4979
|
+
review-pr) cmd_review_pr "$@" ;;
|
|
4753
4980
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
4754
4981
|
help|--help|-h) usage ;;
|
|
4755
4982
|
"") [[ -f "BACKLOG.md" ]] && _dashboard || { usage; _show_changelog; } ;;
|
package/package.json
CHANGED
|
@@ -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.
|