@seanyao/roll 2026.514.5 → 2026.516.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 +11 -0
- package/README.md +2 -0
- package/bin/roll +330 -94
- package/package.json +1 -2
- 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/tools/roll-fetch/SKILL.md +0 -182
- package/tools/roll-fetch/package.json +0 -15
- package/tools/roll-fetch/smart-web-fetch.js +0 -558
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
- **Fixed**: loop 孤儿状态自愈 — cycle 启动时检测 state.yaml 残留 running,若无活跃进程则自动重置为 idle,防止 loop 因中断永久卡死 `[loop]`
|
|
8
|
+
- **New**: 可选的事件驱动 PR 评审模板 — `cp templates/workflows/pr-review-event.yml .github/workflows/`,PR 开即触发 AI 评审,不装也行(loop 每轮兜底) `[pr]`
|
|
9
|
+
- **New**: loop PR inbox 从"分类但空转"升级到"分类+执行" — eligible PR 自动调 AI 评审,stale PR 自动 rebase,fork 和冲突写 ALERT;bot 已评审的 PR 自动让步 `[loop]`
|
|
10
|
+
- **New**: `roll review-pr <number>` — agent-agnostic AI 代码评审,任意 agent(Claude/Kimi/DeepSeek 等)均可评审任意 git 平台的 PR;PR body 加 `[skip-ai-review]` 可跳过 `[pr]`
|
|
11
|
+
- **Fixed**: `roll peer` 终态后 tmux session 不再残留 — AGREE/ESCALATE/UNKNOWN/round≥3 自动 kill,round<3 保留复用 `[peer]`
|
|
12
|
+
- **New**: `loop/cycle-*` 远程僵尸分支兜底 GC — 每轮 cycle 结尾扫描已合入 main 的 `loop/cycle-*` 分支并删除,弥补 PR auto-merge 失败时的清理盲区 `[loop]`
|
|
13
|
+
|
|
3
14
|
## v2026.514.5
|
|
4
15
|
|
|
5
16
|
- **Fixed**: 上版 `claude/*` 临时分支清理意外失效 — 现已恢复 `[loop]`
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
**[中文版 README](README_CN.md)**
|
|
13
13
|
|
|
14
|
+
[](https://seanyao.github.io/Roll/)
|
|
14
15
|
[](LICENSE)
|
|
15
16
|
[](https://www.npmjs.com/package/@seanyao/roll)
|
|
16
17
|
[](https://github.com/seanyao/roll/actions/workflows/ci.yml)
|
|
@@ -64,6 +65,7 @@ roll loop on # optional: let the agent work unattended
|
|
|
64
65
|
| Peer (cross-agent review) | [guide/en/peer.md](docs/guide/en/peer.md) | [guide/zh/peer.md](docs/guide/zh/peer.md) |
|
|
65
66
|
| Configuration (env vars) | [guide/en/configuration.md](docs/guide/en/configuration.md) | [guide/zh/configuration.md](docs/guide/zh/configuration.md) |
|
|
66
67
|
| Skill selection guide | [guide/en/skills.md](docs/guide/en/skills.md) | [guide/zh/skills.md](docs/guide/zh/skills.md) |
|
|
68
|
+
| FAQ (troubleshooting) | [guide/en/faq.md](docs/guide/en/faq.md) | [guide/zh/faq.md](docs/guide/zh/faq.md) |
|
|
67
69
|
| Domain model (DDD) | [domain/context-map.md](docs/domain/context-map.md) | — |
|
|
68
70
|
| Engineering common sense | [practices/engineering-common-sense.md](docs/practices/engineering-common-sense.md) | — |
|
|
69
71
|
|
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.516.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.
|
|
@@ -1112,60 +1131,6 @@ _ensure_features_dir() {
|
|
|
1112
1131
|
_ROLL_MERGE_SUMMARY+=("created|docs/features/")
|
|
1113
1132
|
}
|
|
1114
1133
|
|
|
1115
|
-
# ─── Helper: write starter .gitignore (no-op if exists) ──────────────────────
|
|
1116
|
-
_write_gitignore() {
|
|
1117
|
-
[[ -f "$1" ]] && return
|
|
1118
|
-
cat > "$1" << 'EOF'
|
|
1119
|
-
node_modules/
|
|
1120
|
-
dist/
|
|
1121
|
-
build/
|
|
1122
|
-
.env
|
|
1123
|
-
*.local
|
|
1124
|
-
.DS_Store
|
|
1125
|
-
*.log
|
|
1126
|
-
EOF
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
# ─── Helper: write starter .env.example (no-op if exists) ────────────────────
|
|
1130
|
-
_write_env_example() {
|
|
1131
|
-
[[ -f "$1" ]] && return
|
|
1132
|
-
cat > "$1" << 'EOF'
|
|
1133
|
-
# Environment Variables — copy to .env and fill in values
|
|
1134
|
-
|
|
1135
|
-
# Application
|
|
1136
|
-
# NODE_ENV=development
|
|
1137
|
-
# PORT=3000
|
|
1138
|
-
|
|
1139
|
-
# Database
|
|
1140
|
-
# DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
|
1141
|
-
|
|
1142
|
-
# Auth
|
|
1143
|
-
# JWT_SECRET=your-secret-key
|
|
1144
|
-
EOF
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
# ─── Helper: detect project type from existing AGENTS.md ─────────────────────
|
|
1148
|
-
detect_project_type() {
|
|
1149
|
-
local agents_file="$1/AGENTS.md"
|
|
1150
|
-
[[ -f "$agents_file" ]] || { echo "unknown"; return; }
|
|
1151
|
-
|
|
1152
|
-
local content
|
|
1153
|
-
content="$(cat "$agents_file")"
|
|
1154
|
-
|
|
1155
|
-
if echo "$content" | grep -qi "Fullstack Web"; then
|
|
1156
|
-
echo "fullstack"
|
|
1157
|
-
elif echo "$content" | grep -qi "Backend Service"; then
|
|
1158
|
-
echo "backend-service"
|
|
1159
|
-
elif echo "$content" | grep -qi "Frontend Only"; then
|
|
1160
|
-
echo "frontend-only"
|
|
1161
|
-
elif echo "$content" | grep -qi "CLI Tool"; then
|
|
1162
|
-
echo "cli"
|
|
1163
|
-
else
|
|
1164
|
-
# AGENTS.md exists but has no type marker — fall back to file-based scan
|
|
1165
|
-
scan_project_type_from_files "$1"
|
|
1166
|
-
fi
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1134
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1170
1135
|
# COMMAND: status
|
|
1171
1136
|
# Show current state of conventions
|
|
@@ -1459,7 +1424,7 @@ _peer_auto_attach() {
|
|
|
1459
1424
|
[ "$(uname)" = "Darwin" ] || return 0
|
|
1460
1425
|
[ -f "$_LOOP_MUTE_FILE" ] && return 0
|
|
1461
1426
|
local terminal_pref
|
|
1462
|
-
terminal_pref=$(
|
|
1427
|
+
terminal_pref=$(config_get "loop_attach_terminal" "")
|
|
1463
1428
|
if [[ -z "$terminal_pref" ]]; then
|
|
1464
1429
|
case "${TERM_PROGRAM:-}" in
|
|
1465
1430
|
ghostty) terminal_pref="ghostty" ;;
|
|
@@ -1777,6 +1742,16 @@ cmd_peer() {
|
|
|
1777
1742
|
echo ""
|
|
1778
1743
|
info "Log: $log_file"
|
|
1779
1744
|
|
|
1745
|
+
local _should_kill=true
|
|
1746
|
+
case "$resolution" in
|
|
1747
|
+
REFINE|OBJECT) [[ "$round" -lt 3 ]] && _should_kill=false ;;
|
|
1748
|
+
esac
|
|
1749
|
+
if [[ "$_should_kill" == "true" ]] && [[ -n "$peer_session" ]] \
|
|
1750
|
+
&& command -v tmux >/dev/null 2>&1 \
|
|
1751
|
+
&& tmux has-session -t "$peer_session" 2>/dev/null; then
|
|
1752
|
+
tmux kill-session -t "$peer_session" 2>/dev/null || true
|
|
1753
|
+
fi
|
|
1754
|
+
|
|
1780
1755
|
case "$resolution" in
|
|
1781
1756
|
AGREE) exit 0 ;;
|
|
1782
1757
|
REFINE|OBJECT) exit 2 ;;
|
|
@@ -1898,6 +1873,17 @@ _skill_content() {
|
|
|
1898
1873
|
awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' "$1"
|
|
1899
1874
|
}
|
|
1900
1875
|
|
|
1876
|
+
_parse_review_verdict() {
|
|
1877
|
+
local output="$1"
|
|
1878
|
+
local line
|
|
1879
|
+
line=$(echo "$output" | grep -o '<!--VERDICT:[^>]*-->' | tail -1)
|
|
1880
|
+
[ -n "$line" ] || return 0
|
|
1881
|
+
local type reason
|
|
1882
|
+
type=$(echo "$line" | sed -E 's/<!--VERDICT:([A-Z_]+)(:.*)?-->/\1/')
|
|
1883
|
+
reason=$(echo "$line" | sed -E 's/<!--VERDICT:[A-Z_]+:?//; s/-->//' | sed 's/^ *//')
|
|
1884
|
+
echo "${type}${reason:+:${reason}}"
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1901
1887
|
_agent_run_skill() {
|
|
1902
1888
|
local skill="$1"
|
|
1903
1889
|
local agent; agent=$(_project_agent)
|
|
@@ -1915,6 +1901,102 @@ _agent_run_skill() {
|
|
|
1915
1901
|
esac
|
|
1916
1902
|
}
|
|
1917
1903
|
|
|
1904
|
+
cmd_review_pr() {
|
|
1905
|
+
local pr_number="${1:-}"
|
|
1906
|
+
[ -n "$pr_number" ] || { err "Usage: roll review-pr <number>"; return 1; }
|
|
1907
|
+
|
|
1908
|
+
local slug; slug=$(_gh_repo_slug) || { err "Not a GitHub repo — review-pr requires GitHub remote"; return 1; }
|
|
1909
|
+
|
|
1910
|
+
local pr_json
|
|
1911
|
+
pr_json=$(gh -R "$slug" pr view "$pr_number" --json title,body,diff 2>&1) \
|
|
1912
|
+
|| { err "gh pr view failed: ${pr_json}"; return 1; }
|
|
1913
|
+
|
|
1914
|
+
local title body diff
|
|
1915
|
+
title=$(echo "$pr_json" | jq -r '.title // ""')
|
|
1916
|
+
body=$(echo "$pr_json" | jq -r '.body // ""')
|
|
1917
|
+
diff=$(echo "$pr_json" | jq -r '.diff // ""')
|
|
1918
|
+
|
|
1919
|
+
if echo "$body" | grep -qF '[skip-ai-review]'; then
|
|
1920
|
+
gh -R "$slug" pr review "$pr_number" --approve -b "Auto-approved: [skip-ai-review] detected" 2>/dev/null || true
|
|
1921
|
+
info "PR #${pr_number}: [skip-ai-review] — auto-approved"
|
|
1922
|
+
return 0
|
|
1923
|
+
fi
|
|
1924
|
+
|
|
1925
|
+
local template="${ROLL_PKG_DIR}/skills/roll-review-pr/SKILL.md"
|
|
1926
|
+
[ -f "$template" ] || { err "Skill template not found: ${template}"; return 1; }
|
|
1927
|
+
|
|
1928
|
+
local content; content=$(_skill_content "$template")
|
|
1929
|
+
|
|
1930
|
+
local tmp; tmp=$(mktemp)
|
|
1931
|
+
# shellcheck disable=SC2064
|
|
1932
|
+
trap "rm -f '$tmp'" EXIT
|
|
1933
|
+
|
|
1934
|
+
echo "$content" > "$tmp"
|
|
1935
|
+
sed -i '' "s|{{PR_TITLE}}|${title}|g" "$tmp" 2>/dev/null \
|
|
1936
|
+
|| sed -i "s|{{PR_TITLE}}|${title}|g" "$tmp"
|
|
1937
|
+
|
|
1938
|
+
local body_escaped; body_escaped=$(printf '%s' "$body" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')
|
|
1939
|
+
sed -i '' "s|{{PR_BODY}}|${body_escaped}|g" "$tmp" 2>/dev/null \
|
|
1940
|
+
|| sed -i "s|{{PR_BODY}}|${body_escaped}|g" "$tmp"
|
|
1941
|
+
|
|
1942
|
+
local diff_truncated
|
|
1943
|
+
diff_truncated=$(echo "$diff" | head -500)
|
|
1944
|
+
local diff_file; diff_file=$(mktemp)
|
|
1945
|
+
echo "$diff_truncated" > "$diff_file"
|
|
1946
|
+
awk -v f="$diff_file" '{
|
|
1947
|
+
if (index($0, "{{PR_DIFF}}")) {
|
|
1948
|
+
while ((getline line < f) > 0) print line
|
|
1949
|
+
close(f)
|
|
1950
|
+
} else print
|
|
1951
|
+
}' "$tmp" > "${tmp}.out" && mv "${tmp}.out" "$tmp"
|
|
1952
|
+
rm -f "$diff_file"
|
|
1953
|
+
|
|
1954
|
+
local prompt; prompt=$(cat "$tmp")
|
|
1955
|
+
rm -f "$tmp"
|
|
1956
|
+
trap - EXIT
|
|
1957
|
+
|
|
1958
|
+
local agent; agent=$(_project_agent)
|
|
1959
|
+
local output
|
|
1960
|
+
info "Reviewing PR #${pr_number} with ${agent}..."
|
|
1961
|
+
case "$agent" in
|
|
1962
|
+
claude) output=$(claude -p --output-format text "$prompt" 2>/dev/null) ;;
|
|
1963
|
+
kimi) output=$(kimi --quiet -p "$prompt" 2>/dev/null) ;;
|
|
1964
|
+
deepseek) output=$(deepseek "$prompt" 2>/dev/null) ;;
|
|
1965
|
+
pi) output=$(pi -p "$prompt" 2>/dev/null) ;;
|
|
1966
|
+
codex) output=$(codex exec "$prompt" 2>/dev/null) ;;
|
|
1967
|
+
opencode) output=$(opencode run "$prompt" 2>/dev/null) ;;
|
|
1968
|
+
*) err "Unknown agent '${agent}'"; return 1 ;;
|
|
1969
|
+
esac
|
|
1970
|
+
|
|
1971
|
+
echo "$output"
|
|
1972
|
+
|
|
1973
|
+
local verdict; verdict=$(_parse_review_verdict "$output")
|
|
1974
|
+
local vtype; vtype="${verdict%%:*}"
|
|
1975
|
+
local vreason; vreason="${verdict#*:}"
|
|
1976
|
+
[ "$vreason" = "$vtype" ] && vreason=""
|
|
1977
|
+
|
|
1978
|
+
case "$vtype" in
|
|
1979
|
+
APPROVE)
|
|
1980
|
+
gh -R "$slug" pr review "$pr_number" --approve -b "AI review: approved" 2>/dev/null || true
|
|
1981
|
+
info "PR #${pr_number}: APPROVED"
|
|
1982
|
+
;;
|
|
1983
|
+
REQUEST_CHANGES)
|
|
1984
|
+
gh -R "$slug" pr review "$pr_number" --request-changes -b "${vreason:-AI review requested changes}" 2>/dev/null || true
|
|
1985
|
+
info "PR #${pr_number}: REQUEST_CHANGES — ${vreason}"
|
|
1986
|
+
;;
|
|
1987
|
+
UNCERTAIN)
|
|
1988
|
+
warn "PR #${pr_number}: UNCERTAIN — ${vreason}"
|
|
1989
|
+
local alert_file="${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/ALERT.md"
|
|
1990
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
1991
|
+
printf '[%s] PR #%s: AI review UNCERTAIN — %s\n' \
|
|
1992
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr_number" "$vreason" >> "$alert_file"
|
|
1993
|
+
;;
|
|
1994
|
+
*)
|
|
1995
|
+
warn "PR #${pr_number}: no verdict parsed from agent output"
|
|
1996
|
+
;;
|
|
1997
|
+
esac
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1918
2000
|
cmd_agent() {
|
|
1919
2001
|
local subcmd="${1:-}"; shift || true
|
|
1920
2002
|
case "$subcmd" in
|
|
@@ -1929,8 +2011,12 @@ cmd_agent() {
|
|
|
1929
2011
|
fi
|
|
1930
2012
|
ok "Agent set to ${name} for this project 当前项目 agent 已设为 ${name}"
|
|
1931
2013
|
local project_path; project_path=$(pwd -P)
|
|
1932
|
-
|
|
1933
|
-
|
|
2014
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2015
|
+
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
2016
|
+
if [[ -f "$runner" ]]; then
|
|
2017
|
+
_install_launchd_plists "$project_path" >/dev/null
|
|
2018
|
+
ok "Loop runner scripts regenerated for new agent 已为新 agent 重新生成 loop 脚本"
|
|
2019
|
+
fi
|
|
1934
2020
|
;;
|
|
1935
2021
|
list)
|
|
1936
2022
|
echo ""; echo " Available agents 可用 agent:"; echo ""
|
|
@@ -1971,6 +2057,14 @@ _LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
|
1971
2057
|
# hash of the full path, ensuring uniqueness across sibling dirs with same name.
|
|
1972
2058
|
_project_slug() {
|
|
1973
2059
|
local path="$1"
|
|
2060
|
+
# FIX-034: when inside a git worktree, git-common-dir returns the main tree's
|
|
2061
|
+
# absolute .git path; resolve to the main tree so worktree and main-tree runs
|
|
2062
|
+
# produce the same slug.
|
|
2063
|
+
local _common
|
|
2064
|
+
_common=$(git -C "$path" rev-parse --git-common-dir 2>/dev/null)
|
|
2065
|
+
if [[ -n "$_common" && "$_common" == *"/.git" ]]; then
|
|
2066
|
+
path="${_common%/.git}"
|
|
2067
|
+
fi
|
|
1974
2068
|
local base; base=$(basename "$path")
|
|
1975
2069
|
local hash
|
|
1976
2070
|
if command -v md5 &>/dev/null; then
|
|
@@ -1985,17 +2079,10 @@ _project_slug() {
|
|
|
1985
2079
|
_config_read_int() {
|
|
1986
2080
|
local key="$1" default="$2"
|
|
1987
2081
|
local val
|
|
1988
|
-
val=$(
|
|
2082
|
+
val=$(config_get "$key" "")
|
|
1989
2083
|
if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val"; else echo "$default"; fi
|
|
1990
2084
|
}
|
|
1991
2085
|
|
|
1992
|
-
_config_read_string() {
|
|
1993
|
-
local key="$1" default="$2"
|
|
1994
|
-
local val
|
|
1995
|
-
val=$(grep "^${key}:" "$ROLL_CONFIG" 2>/dev/null | awk '{print $2}' | tr -d '"' | head -1)
|
|
1996
|
-
if [[ -n "$val" ]]; then echo "$val"; else echo "$default"; fi
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
2086
|
# Derive a minute in [1,55] from project path hash + offset so different projects
|
|
2000
2087
|
# and different services within a project don't fire at the same time.
|
|
2001
2088
|
# Offsets used: loop=0, dream=2, brief=4 → always three distinct values (2<55).
|
|
@@ -2108,7 +2195,15 @@ if [ -f "\$INNER_LOCK" ]; then
|
|
|
2108
2195
|
rm -f "\$INNER_LOCK"
|
|
2109
2196
|
fi
|
|
2110
2197
|
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
2111
|
-
|
|
2198
|
+
# FIX-038: background heartbeat writer — outer script uses this as primary liveness signal
|
|
2199
|
+
# to detect stale execution without relying on PID reuse heuristics.
|
|
2200
|
+
HEARTBEAT_FILE="${HOME}/.shared/roll/loop/.heartbeat-${slug}"
|
|
2201
|
+
_heartbeat_writer() {
|
|
2202
|
+
while true; do echo "\$(date -u +%s)" > "\$HEARTBEAT_FILE"; sleep 60; done
|
|
2203
|
+
}
|
|
2204
|
+
_heartbeat_writer &
|
|
2205
|
+
_HEARTBEAT_PID=\$!
|
|
2206
|
+
trap 'kill "\${_HEARTBEAT_PID}" 2>/dev/null; rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"' EXIT
|
|
2112
2207
|
|
|
2113
2208
|
# US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
|
|
2114
2209
|
# safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
|
|
@@ -2166,37 +2261,50 @@ _claude_cleanup_stale_worktrees "${project_path}" || true
|
|
|
2166
2261
|
# When \`gh\` is unavailable, fall back to the legacy ff-merge path.
|
|
2167
2262
|
if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
2168
2263
|
if [ "\$_exit" -eq 0 ]; then
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
fi
|
|
2174
|
-
_publish_status=\$?
|
|
2175
|
-
if [ "\$_publish_status" -eq 0 ]; then
|
|
2264
|
+
# Idle cycle — no commits ahead of origin/main means nothing was built;
|
|
2265
|
+
# skip publish and reclaim the worktree immediately.
|
|
2266
|
+
_cycle_commits=\$(cd "\$WT" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
2267
|
+
if [ "\$_cycle_commits" -eq 0 ]; then
|
|
2176
2268
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2177
|
-
echo "[loop] cycle \${CYCLE_ID}:
|
|
2178
|
-
|
|
2179
|
-
if ( cd "
|
|
2269
|
+
echo "[loop] cycle \${CYCLE_ID}: idle (no new commits); worktree cleaned"
|
|
2270
|
+
else
|
|
2271
|
+
if ( cd "\$WT" && _loop_is_doc_only_change ); then
|
|
2272
|
+
( cd "\$WT" && _loop_publish_doc_pr "\$BRANCH" "doc: loop cycle \${CYCLE_ID}" )
|
|
2273
|
+
else
|
|
2274
|
+
( cd "\$WT" && _loop_publish_pr "\$BRANCH" "loop cycle \${CYCLE_ID}" )
|
|
2275
|
+
fi
|
|
2276
|
+
_publish_status=\$?
|
|
2277
|
+
if [ "\$_publish_status" -eq 0 ]; then
|
|
2180
2278
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2181
|
-
echo "[loop] cycle \${CYCLE_ID}:
|
|
2279
|
+
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
2280
|
+
elif [ "\$_publish_status" -eq 2 ]; then
|
|
2281
|
+
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
2282
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2283
|
+
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2284
|
+
else
|
|
2285
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh unavailable AND merge_back failed; worktree preserved at \$WT"
|
|
2286
|
+
echo "[loop] cycle \${CYCLE_ID}: gh+merge_back both failed; worktree preserved at \$WT"
|
|
2287
|
+
fi
|
|
2182
2288
|
else
|
|
2183
|
-
_worktree_alert "cycle \${CYCLE_ID}:
|
|
2184
|
-
echo "[loop] cycle \${CYCLE_ID}:
|
|
2289
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2290
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2185
2291
|
fi
|
|
2186
|
-
else
|
|
2187
|
-
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2188
|
-
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2189
2292
|
fi
|
|
2190
2293
|
else
|
|
2191
2294
|
_worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2192
2295
|
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2193
2296
|
fi
|
|
2194
2297
|
fi
|
|
2298
|
+
|
|
2299
|
+
# US-AUTO-040: fallback GC — delete remote loop/cycle-* branches already merged to main.
|
|
2300
|
+
_loop_cleanup_stale_cycle_branches "${project_path}" || true
|
|
2195
2301
|
INNER
|
|
2196
2302
|
chmod +x "$inner_path"
|
|
2197
2303
|
|
|
2198
2304
|
cat > "$script_path" << SCRIPT
|
|
2199
2305
|
#!/bin/bash -l
|
|
2306
|
+
# caffeinate: prevent idle sleep from killing claude during cycles
|
|
2307
|
+
caffeinate -i -w \$\$ &
|
|
2200
2308
|
# Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
|
|
2201
2309
|
if [ -z "\$ROLL_LOOP_FORCE" ]; then
|
|
2202
2310
|
h=\$(printf '%d' "\$(date +%H)")
|
|
@@ -2205,6 +2313,50 @@ fi
|
|
|
2205
2313
|
# Pause check — 'roll loop pause' creates this marker to suspend scheduling
|
|
2206
2314
|
PAUSE="\$HOME/.shared/roll/loop/PAUSE-${slug}"
|
|
2207
2315
|
if [ -z "\$ROLL_LOOP_FORCE" ] && [ -f "\$PAUSE" ]; then exit 0; fi
|
|
2316
|
+
# FIX-037: orphan state detection & self-heal — if state.yaml says running
|
|
2317
|
+
# but no LOCK process or tmux session exists, the previous cycle was killed
|
|
2318
|
+
# (e.g. SIGKILL / sleep / terminal close). Heal state to idle so the next
|
|
2319
|
+
# cycle can proceed normally; write ALERT for transparency.
|
|
2320
|
+
# FIX-038: heartbeat is the primary liveness signal (avoids PID reuse race);
|
|
2321
|
+
# LOCK pid check is secondary fallback for backward compatibility.
|
|
2322
|
+
HEARTBEAT_TIMEOUT="\${ROLL_HEARTBEAT_TIMEOUT:-1800}"
|
|
2323
|
+
STATE_FILE="${HOME}/.shared/roll/loop/state.yaml"
|
|
2324
|
+
if [ -f "\$STATE_FILE" ]; then
|
|
2325
|
+
_state=\$(grep '^status:' "\$STATE_FILE" | awk '{print \$2}' 2>/dev/null || echo "")
|
|
2326
|
+
if [ "\$_state" = "running" ]; then
|
|
2327
|
+
_still_active=false
|
|
2328
|
+
# FIX-038: heartbeat is primary signal
|
|
2329
|
+
_heartbeat_file="${HOME}/.shared/roll/loop/.heartbeat-${slug}"
|
|
2330
|
+
if [ -f "\$_heartbeat_file" ]; then
|
|
2331
|
+
_hb_ts=\$(cat "\$_heartbeat_file" 2>/dev/null || echo "0")
|
|
2332
|
+
_now=\$(date -u +%s)
|
|
2333
|
+
_hb_age=\$(( _now - _hb_ts ))
|
|
2334
|
+
if [ "\$_hb_age" -lt "\$HEARTBEAT_TIMEOUT" ]; then
|
|
2335
|
+
_still_active=true
|
|
2336
|
+
fi
|
|
2337
|
+
fi
|
|
2338
|
+
# Fallback: LOCK pid check (for cycles without heartbeat, e.g. pre-FIX-038)
|
|
2339
|
+
if [ "\$_still_active" = false ]; then
|
|
2340
|
+
_lock_file="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
2341
|
+
if [ -f "\$_lock_file" ]; then
|
|
2342
|
+
_lock_pid=\$(head -1 "\$_lock_file" 2>/dev/null || echo "")
|
|
2343
|
+
[ -n "\$_lock_pid" ] && kill -0 "\$_lock_pid" 2>/dev/null && _still_active=true
|
|
2344
|
+
fi
|
|
2345
|
+
fi
|
|
2346
|
+
# Final: tmux session check
|
|
2347
|
+
if [ "\$_still_active" = false ]; then
|
|
2348
|
+
command -v tmux >/dev/null 2>&1 && tmux has-session -t "roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')" 2>/dev/null && _still_active=true
|
|
2349
|
+
fi
|
|
2350
|
+
if [ "\$_still_active" = false ]; then
|
|
2351
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] FIX-037: orphan state detected (status=running, heartbeat stale or missing) — healing to idle" >> "\$LOG"
|
|
2352
|
+
echo "status: idle" > "\${STATE_FILE}.tmp" && mv "\${STATE_FILE}.tmp" "\$STATE_FILE"
|
|
2353
|
+
rm -f "\$_lock_file" 2>/dev/null || true
|
|
2354
|
+
_alert_file="\$(dirname "\$0")/ALERT.md"
|
|
2355
|
+
echo "\$(date '+%Y-%m-%dT%H:%M:%S%z') | FIX-037 auto-heal | Orphan state detected and cleared (status=running → idle)" >> "\$_alert_file" 2>/dev/null || true
|
|
2356
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] FIX-037: healed to idle, ALERT written" >> "\$LOG"
|
|
2357
|
+
fi
|
|
2358
|
+
fi
|
|
2359
|
+
fi
|
|
2208
2360
|
LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
2209
2361
|
SESSION="roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
2210
2362
|
INNER_SCRIPT="${inner_path}"
|
|
@@ -2227,7 +2379,9 @@ fi
|
|
|
2227
2379
|
echo "\$\$" > "\$LOCK"
|
|
2228
2380
|
trap 'rm -f "\$LOCK"' EXIT
|
|
2229
2381
|
if command -v tmux >/dev/null 2>&1; then
|
|
2230
|
-
tmux
|
|
2382
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-" | while read _s; do
|
|
2383
|
+
tmux kill-session -t "\$_s" 2>/dev/null || true
|
|
2384
|
+
done
|
|
2231
2385
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
2232
2386
|
tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
|
|
2233
2387
|
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
@@ -2304,7 +2458,7 @@ _install_launchd_plists() {
|
|
|
2304
2458
|
|
|
2305
2459
|
# Terminal preference: config wins, then TERM_PROGRAM env, then "Terminal"
|
|
2306
2460
|
local terminal_pref
|
|
2307
|
-
terminal_pref=$(
|
|
2461
|
+
terminal_pref=$(config_get "loop_attach_terminal" "")
|
|
2308
2462
|
if [[ -z "$terminal_pref" ]]; then
|
|
2309
2463
|
case "${TERM_PROGRAM:-}" in
|
|
2310
2464
|
ghostty) terminal_pref="ghostty" ;;
|
|
@@ -2534,7 +2688,7 @@ _loop_test() {
|
|
|
2534
2688
|
|
|
2535
2689
|
# Detect terminal pref same way _install_launchd_plists does
|
|
2536
2690
|
local terminal_pref
|
|
2537
|
-
terminal_pref=$(
|
|
2691
|
+
terminal_pref=$(config_get "loop_attach_terminal" "")
|
|
2538
2692
|
if [[ -z "$terminal_pref" ]]; then
|
|
2539
2693
|
case "${TERM_PROGRAM:-}" in
|
|
2540
2694
|
ghostty) terminal_pref="ghostty" ;;
|
|
@@ -3319,6 +3473,55 @@ _loop_pr_state_write() {
|
|
|
3319
3473
|
' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
3320
3474
|
}
|
|
3321
3475
|
|
|
3476
|
+
# _loop_pr_review_external <pr_number>
|
|
3477
|
+
# Calls cmd_review_pr (US-PR-001) to run AI review on an eligible external PR.
|
|
3478
|
+
# Lenient: errors are logged but do not fail the loop.
|
|
3479
|
+
_loop_pr_review_external() {
|
|
3480
|
+
local pr="$1"
|
|
3481
|
+
[ -n "$pr" ] || return 0
|
|
3482
|
+
cmd_review_pr "$pr" 2>&1 || {
|
|
3483
|
+
warn "review-pr failed for PR #${pr} (non-fatal)"
|
|
3484
|
+
return 0
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
# _loop_pr_rebase_stale <pr_number> <head_ref>
|
|
3489
|
+
# Attempts to rebase a stale PR onto origin/main and push.
|
|
3490
|
+
# Fork PRs are skipped (no write access). Conflicts write ALERT.
|
|
3491
|
+
_loop_pr_rebase_stale() {
|
|
3492
|
+
local pr="$1" head_ref="$2"
|
|
3493
|
+
[ -n "$pr" ] && [ -n "$head_ref" ] || return 0
|
|
3494
|
+
|
|
3495
|
+
local slug; slug=$(_gh_repo_slug 2>/dev/null) || return 0
|
|
3496
|
+
|
|
3497
|
+
local pr_json
|
|
3498
|
+
pr_json=$(gh -R "$slug" pr view "$pr" --json headRepository,headRepositoryOwner,isCrossRepository 2>/dev/null) || return 0
|
|
3499
|
+
local is_fork
|
|
3500
|
+
is_fork=$(echo "$pr_json" | jq -r '.isCrossRepository // false' 2>/dev/null)
|
|
3501
|
+
if [ "$is_fork" = "true" ]; then
|
|
3502
|
+
local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
|
|
3503
|
+
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3504
|
+
printf '[%s] PR #%s: fork PR — cannot rebase (no write access)\n' \
|
|
3505
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" >> "$alert"
|
|
3506
|
+
return 0
|
|
3507
|
+
fi
|
|
3508
|
+
|
|
3509
|
+
git fetch origin "$head_ref" 2>/dev/null || return 0
|
|
3510
|
+
if git checkout "$head_ref" 2>/dev/null \
|
|
3511
|
+
&& git rebase origin/main 2>/dev/null \
|
|
3512
|
+
&& git push origin "$head_ref" 2>/dev/null; then
|
|
3513
|
+
info "PR #${pr}: rebased ${head_ref} onto origin/main"
|
|
3514
|
+
else
|
|
3515
|
+
git rebase --abort 2>/dev/null || true
|
|
3516
|
+
git checkout - 2>/dev/null || true
|
|
3517
|
+
local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
|
|
3518
|
+
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3519
|
+
printf '[%s] PR #%s: rebase conflict on %s — please rebase manually\n' \
|
|
3520
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" "$head_ref" >> "$alert"
|
|
3521
|
+
fi
|
|
3522
|
+
return 0
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3322
3525
|
# _loop_pr_inbox
|
|
3323
3526
|
# Walks open PRs and routes each by classification.
|
|
3324
3527
|
# Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
|
|
@@ -3348,10 +3551,13 @@ _loop_pr_inbox() {
|
|
|
3348
3551
|
--json reviews,mergeStateStatus,statusCheckRollup \
|
|
3349
3552
|
2>/dev/null) || { i=$((i + 1)); continue; }
|
|
3350
3553
|
|
|
3351
|
-
local human_review ci_state mergeable
|
|
3554
|
+
local human_review ci_state mergeable bot_review
|
|
3352
3555
|
human_review=$(echo "$view_json" | jq -r '
|
|
3353
3556
|
[.reviews[]? | select(.authorAssociation != "BOT" and .authorAssociation != "APP")]
|
|
3354
3557
|
| last // {} | .state // ""' 2>/dev/null)
|
|
3558
|
+
bot_review=$(echo "$view_json" | jq -r '
|
|
3559
|
+
[.reviews[]? | select(.authorAssociation == "BOT" or .authorAssociation == "APP")]
|
|
3560
|
+
| last // {} | .state // ""' 2>/dev/null)
|
|
3355
3561
|
mergeable=$(echo "$view_json" | jq -r '.mergeStateStatus // ""' 2>/dev/null)
|
|
3356
3562
|
ci_state=$(echo "$view_json" | jq -r '
|
|
3357
3563
|
if (.statusCheckRollup | length) == 0 then ""
|
|
@@ -3359,6 +3565,17 @@ _loop_pr_inbox() {
|
|
|
3359
3565
|
elif all(.statusCheckRollup[]?; .conclusion == "SUCCESS" or .conclusion == "SKIPPED") then "success"
|
|
3360
3566
|
else "pending" end' 2>/dev/null)
|
|
3361
3567
|
|
|
3568
|
+
# Bot review gate: if a GHA workflow already handled this PR, defer to it.
|
|
3569
|
+
if [ "$bot_review" = "APPROVED" ]; then
|
|
3570
|
+
i=$((i + 1)); continue
|
|
3571
|
+
elif [ "$bot_review" = "CHANGES_REQUESTED" ]; then
|
|
3572
|
+
local alert="${_LOOP_ALERT:-${HOME}/.shared/roll/loop/ALERT.md}"
|
|
3573
|
+
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3574
|
+
printf '[%s] PR #%s: bot review CHANGES_REQUESTED — loop PR rejected by GHA reviewer\n' \
|
|
3575
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$num" >> "$alert"
|
|
3576
|
+
i=$((i + 1)); continue
|
|
3577
|
+
fi
|
|
3578
|
+
|
|
3362
3579
|
local verdict
|
|
3363
3580
|
verdict=$(_loop_pr_classify "$head_ref" "$human_review" "$ci_state" "$mergeable")
|
|
3364
3581
|
|
|
@@ -3368,15 +3585,10 @@ _loop_pr_inbox() {
|
|
|
3368
3585
|
;;
|
|
3369
3586
|
stale)
|
|
3370
3587
|
_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
|
|
3588
|
+
_loop_pr_rebase_stale "$num" "$head_ref" || true
|
|
3375
3589
|
;;
|
|
3376
3590
|
eligible)
|
|
3377
|
-
|
|
3378
|
-
command -v _loop_pr_review_external >/dev/null 2>&1 \
|
|
3379
|
-
&& _loop_pr_review_external "$num" || true
|
|
3591
|
+
_loop_pr_review_external "$num" || true
|
|
3380
3592
|
;;
|
|
3381
3593
|
esac
|
|
3382
3594
|
|
|
@@ -3715,6 +3927,28 @@ _claude_cleanup_stale_worktrees() {
|
|
|
3715
3927
|
return 0
|
|
3716
3928
|
}
|
|
3717
3929
|
|
|
3930
|
+
_loop_cleanup_stale_cycle_branches() {
|
|
3931
|
+
local project_path="${1:-.}"
|
|
3932
|
+
local url; url=$(git -C "$project_path" remote get-url origin 2>/dev/null) || return 0
|
|
3933
|
+
[[ "$url" == *github.com* ]] || return 0
|
|
3934
|
+
|
|
3935
|
+
local branches
|
|
3936
|
+
branches=$(git -C "$project_path" ls-remote --heads origin 'refs/heads/loop/cycle-*' 2>/dev/null \
|
|
3937
|
+
| awk '{print $2}' | sed 's|^refs/heads/||')
|
|
3938
|
+
[ -z "$branches" ] && return 0
|
|
3939
|
+
|
|
3940
|
+
while IFS= read -r branch; do
|
|
3941
|
+
[ -z "$branch" ] && continue
|
|
3942
|
+
if ! git -C "$project_path" merge-base --is-ancestor "$branch" origin/main 2>/dev/null; then
|
|
3943
|
+
continue
|
|
3944
|
+
fi
|
|
3945
|
+
if git -C "$project_path" push origin --delete "$branch" 2>/dev/null; then
|
|
3946
|
+
echo "[loop] deleted stale cycle branch: $branch"
|
|
3947
|
+
fi
|
|
3948
|
+
done <<< "$branches"
|
|
3949
|
+
return 0
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3718
3952
|
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
3719
3953
|
#
|
|
3720
3954
|
# _loop_publish_pr <branch> [title]
|
|
@@ -4719,6 +4953,7 @@ usage() {
|
|
|
4719
4953
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
4720
4954
|
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
4721
4955
|
echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
|
|
4956
|
+
echo " review-pr <number> [PR Review] AI-powered code review for a PR AI 代码评审"
|
|
4722
4957
|
echo ""
|
|
4723
4958
|
echo "Examples / 示例:"
|
|
4724
4959
|
echo " roll setup # New machine: first-time install 新机器首次安装"
|
|
@@ -4750,6 +4985,7 @@ main() {
|
|
|
4750
4985
|
agent) cmd_agent "$@" ;;
|
|
4751
4986
|
release) cmd_release "$@" ;;
|
|
4752
4987
|
ci) cmd_ci "$@" ;;
|
|
4988
|
+
review-pr) cmd_review_pr "$@" ;;
|
|
4753
4989
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
4754
4990
|
help|--help|-h) usage ;;
|
|
4755
4991
|
"") [[ -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.
|
|
3
|
+
"version": "2026.516.1",
|
|
4
4
|
"description": "Roll — Roll out features with AI agents",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "bash tests/run.sh"
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"conventions/",
|
|
28
28
|
"lib/",
|
|
29
29
|
"skills/",
|
|
30
|
-
"tools/",
|
|
31
30
|
"template/",
|
|
32
31
|
"README.md",
|
|
33
32
|
"CHANGELOG.md"
|
|
@@ -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 已加但日志找不到"或反过来的撕裂状态
|