@seanyao/roll 2026.517.3 → 2026.517.4
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/bin/roll +106 -197
- package/lib/loop-fmt.py +82 -7
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +4 -0
- package/skills/roll-.review/SKILL.md +32 -1
- package/skills/roll-build/SKILL.md +33 -15
- package/skills/roll-loop/SKILL.md +33 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.517.4
|
|
4
|
+
- **Improved**: `roll-build` story 收尾三角度并行深审(重用/质量/效率),自检清单新增参数膨胀、N+1 等具体反模式
|
|
5
|
+
- **New**: 七个功能区补上中英双语用户指南 — 现在都能从文档里找到了 `[dream]`
|
|
6
|
+
- **Improved**: 官网首屏动画 — 6 秒内演示装好到自动交付的完整流程 `[loop]`
|
|
7
|
+
- **Added**: `features.md` 现在区分已上线和规划中的功能 — 一眼看出哪些能用
|
|
8
|
+
- **Improved**: loop attach 输出不再像卡住 — 执行 story、等 CI、合 PR 三个等待点都有动态反馈 `[loop]`
|
|
9
|
+
- **Fixed**: 恢复孤儿 worktree 时 PR 不再被 BEHIND 状态卡住 `[loop]`
|
|
10
|
+
- **Fixed**: 无 PR 时 `roll ci --wait` 不再一直等到超时 `[loop]`
|
|
11
|
+
- **Fixed**: loop 现在等 PR 合入 main 才算交付,不再 CI 绿就认为代码进了主干 `[loop]`
|
|
12
|
+
|
|
3
13
|
## v2026.517.3
|
|
4
14
|
- **New**: dream 现在察觉功能目录过期 — 落后时不再悄悄无人知晓 `[dream]`
|
|
5
15
|
- **New**: `roll loop events` — 查看 loop 每轮的详细事件流,任务选择、评审结果、CI 状态、合并全都有迹可查 `[loop]`
|
package/bin/roll
CHANGED
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Roll — AI Agent Convention Manager
|
|
5
5
|
# Single source of truth for how all AI coding agents behave.
|
|
6
6
|
|
|
7
|
-
VERSION="2026.517.
|
|
7
|
+
VERSION="2026.517.4"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -872,167 +872,6 @@ cmd_init() {
|
|
|
872
872
|
fi
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
|
|
876
|
-
# ─── Helper: merge global preamble + template into output ────────────────────
|
|
877
|
-
merge_convention() {
|
|
878
|
-
local filename="$1"
|
|
879
|
-
local tpl_dir="$2"
|
|
880
|
-
local out_dir="$3"
|
|
881
|
-
local display="${4:-$filename}"
|
|
882
|
-
local global_file="$ROLL_GLOBAL/$filename"
|
|
883
|
-
local tpl_file="$tpl_dir/$filename"
|
|
884
|
-
local out_file="$out_dir/$filename"
|
|
885
|
-
|
|
886
|
-
if [[ ! -f "$tpl_file" ]]; then
|
|
887
|
-
return
|
|
888
|
-
fi
|
|
889
|
-
|
|
890
|
-
local merged
|
|
891
|
-
merged="$(cat "$global_file" 2>/dev/null || true)"
|
|
892
|
-
merged+=$'\n\n'
|
|
893
|
-
merged+="---"
|
|
894
|
-
merged+=$'\n\n'
|
|
895
|
-
merged+="$(cat "$tpl_file")"
|
|
896
|
-
|
|
897
|
-
if [[ -f "$out_file" ]]; then
|
|
898
|
-
if diff -q <(echo "$merged") "$out_file" &>/dev/null; then
|
|
899
|
-
_ROLL_MERGE_SUMMARY+=("unchanged|$display")
|
|
900
|
-
return # identical, nothing to do
|
|
901
|
-
fi
|
|
902
|
-
warn "File exists: $display 文件已存在: $display"
|
|
903
|
-
echo -n " [o] Overwrite [k] Keep [M] Merge (default): "
|
|
904
|
-
read -r answer
|
|
905
|
-
answer="${answer:-M}"
|
|
906
|
-
case "$answer" in
|
|
907
|
-
o|O)
|
|
908
|
-
echo "$merged" > "$out_file"
|
|
909
|
-
ok "Overwritten: $display 已覆盖: $display"
|
|
910
|
-
_ROLL_MERGE_SUMMARY+=("overwritten|$display")
|
|
911
|
-
return
|
|
912
|
-
;;
|
|
913
|
-
k|K)
|
|
914
|
-
info "Kept: $display 已保留: $display"
|
|
915
|
-
_ROLL_MERGE_SUMMARY+=("kept|$display")
|
|
916
|
-
return
|
|
917
|
-
;;
|
|
918
|
-
m|M|"")
|
|
919
|
-
# Merge: for each ## section in template:
|
|
920
|
-
# - if missing in output → append
|
|
921
|
-
# - if present but content differs → show diff, prompt [u/k]
|
|
922
|
-
# - if present and identical → skip
|
|
923
|
-
|
|
924
|
-
# Helper: extract a section's body (lines after heading until next ## or EOF)
|
|
925
|
-
# Usage: _extract_section "$file" "$heading"
|
|
926
|
-
_extract_section() {
|
|
927
|
-
local file="$1"
|
|
928
|
-
local heading="$2"
|
|
929
|
-
local in_section=false
|
|
930
|
-
local body=""
|
|
931
|
-
while IFS= read -r ln; do
|
|
932
|
-
if [[ "$ln" == "$heading" ]]; then
|
|
933
|
-
in_section=true
|
|
934
|
-
continue
|
|
935
|
-
fi
|
|
936
|
-
if [[ "$in_section" == "true" ]]; then
|
|
937
|
-
if [[ "$ln" =~ ^##\ ]]; then
|
|
938
|
-
break
|
|
939
|
-
fi
|
|
940
|
-
body+="$ln"$'\n'
|
|
941
|
-
fi
|
|
942
|
-
done < "$file"
|
|
943
|
-
printf '%s' "$body"
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
local current_heading=""
|
|
947
|
-
local current_body=""
|
|
948
|
-
|
|
949
|
-
# _process_section: check/append/update a completed section
|
|
950
|
-
_process_section() {
|
|
951
|
-
local heading="$1"
|
|
952
|
-
local body="$2"
|
|
953
|
-
local out="$3"
|
|
954
|
-
if ! grep -qF "$heading" "$out" 2>/dev/null; then
|
|
955
|
-
# Section missing → append
|
|
956
|
-
printf '\n%s\n%s' "$heading" "$body" >> "$out"
|
|
957
|
-
else
|
|
958
|
-
# Section exists → compare content
|
|
959
|
-
local existing
|
|
960
|
-
existing="$(_extract_section "$out" "$heading")"
|
|
961
|
-
if [[ "$body" != "$existing" ]]; then
|
|
962
|
-
echo ""
|
|
963
|
-
warn "Section \"$heading\" exists but content differs:"
|
|
964
|
-
diff <(printf '%s' "$existing") <(printf '%s' "$body") || true
|
|
965
|
-
echo -n " [u] update with template [k] keep mine (default): "
|
|
966
|
-
local sec_ans
|
|
967
|
-
read -r sec_ans
|
|
968
|
-
sec_ans="${sec_ans:-k}"
|
|
969
|
-
if [[ "$sec_ans" == "u" || "$sec_ans" == "U" ]]; then
|
|
970
|
-
# Replace existing section with template section
|
|
971
|
-
local tmp_out
|
|
972
|
-
tmp_out="$(mktemp)"
|
|
973
|
-
local skip=false
|
|
974
|
-
while IFS= read -r fline; do
|
|
975
|
-
if [[ "$fline" == "$heading" ]]; then
|
|
976
|
-
skip=true
|
|
977
|
-
printf '%s\n%s' "$heading" "$body" >> "$tmp_out"
|
|
978
|
-
continue
|
|
979
|
-
fi
|
|
980
|
-
if [[ "$skip" == "true" ]]; then
|
|
981
|
-
if [[ "$fline" =~ ^##\ ]]; then
|
|
982
|
-
skip=false
|
|
983
|
-
printf '%s\n' "$fline" >> "$tmp_out"
|
|
984
|
-
fi
|
|
985
|
-
continue
|
|
986
|
-
fi
|
|
987
|
-
printf '%s\n' "$fline" >> "$tmp_out"
|
|
988
|
-
done < "$out"
|
|
989
|
-
mv "$tmp_out" "$out"
|
|
990
|
-
fi
|
|
991
|
-
fi
|
|
992
|
-
fi
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
# Parse template sections, reading from file (not herestring) to keep stdin free
|
|
996
|
-
# for interactive user prompts inside _process_section
|
|
997
|
-
local tpl_sections=()
|
|
998
|
-
local tpl_bodies=()
|
|
999
|
-
local cur_h="" cur_b=""
|
|
1000
|
-
while IFS= read -r line; do
|
|
1001
|
-
if [[ "$line" =~ ^##\ ]]; then
|
|
1002
|
-
if [[ -n "$cur_h" ]]; then
|
|
1003
|
-
tpl_sections+=("$cur_h")
|
|
1004
|
-
tpl_bodies+=("$cur_b")
|
|
1005
|
-
fi
|
|
1006
|
-
cur_h="$line"
|
|
1007
|
-
cur_b=""
|
|
1008
|
-
elif [[ -n "$cur_h" ]]; then
|
|
1009
|
-
cur_b+="$line"$'\n'
|
|
1010
|
-
fi
|
|
1011
|
-
done < "$tpl_file"
|
|
1012
|
-
# Capture last section
|
|
1013
|
-
if [[ -n "$cur_h" ]]; then
|
|
1014
|
-
tpl_sections+=("$cur_h")
|
|
1015
|
-
tpl_bodies+=("$cur_b")
|
|
1016
|
-
fi
|
|
1017
|
-
|
|
1018
|
-
# Now process sections (stdin is free for user input)
|
|
1019
|
-
local i
|
|
1020
|
-
for (( i=0; i<${#tpl_sections[@]}; i++ )); do
|
|
1021
|
-
_process_section "${tpl_sections[$i]}" "${tpl_bodies[$i]}" "$out_file"
|
|
1022
|
-
done
|
|
1023
|
-
|
|
1024
|
-
ok "Merged: $display 已合并: $display"
|
|
1025
|
-
_ROLL_MERGE_SUMMARY+=("merged|$display")
|
|
1026
|
-
return
|
|
1027
|
-
;;
|
|
1028
|
-
esac
|
|
1029
|
-
fi
|
|
1030
|
-
|
|
1031
|
-
echo "$merged" > "$out_file"
|
|
1032
|
-
ok "Created: $display 已创建: $display"
|
|
1033
|
-
_ROLL_MERGE_SUMMARY+=("created|$display")
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
875
|
# ─── Helper: print a tidy summary of merge actions ───────────────────────────
|
|
1037
876
|
print_merge_summary() {
|
|
1038
877
|
if [[ ${#_ROLL_MERGE_SUMMARY[@]} -eq 0 ]]; then
|
|
@@ -2243,6 +2082,13 @@ _write_loop_runner_script() {
|
|
|
2243
2082
|
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
2244
2083
|
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
2245
2084
|
local claude_cmd; claude_cmd="${cmd_verbose#cd \"*\" && }"
|
|
2085
|
+
# FIX-048: Claude Code resolves project root from the worktree's .git file to
|
|
2086
|
+
# the main repo, placing worktree absolute paths outside its sandbox. Inject
|
|
2087
|
+
# --add-dir "$WT" so the worktree directory is explicitly allowed. Only applies
|
|
2088
|
+
# to claude (the --output-format stream-json flag is exclusive to claude runs).
|
|
2089
|
+
if [[ "$claude_cmd" == *"--output-format stream-json"* ]]; then
|
|
2090
|
+
claude_cmd="${claude_cmd/--output-format stream-json/--output-format stream-json --add-dir \"\$WT\"}"
|
|
2091
|
+
fi
|
|
2246
2092
|
local slug; slug=$(_project_slug "$project_path")
|
|
2247
2093
|
cat > "$inner_path" << INNER
|
|
2248
2094
|
#!/bin/bash -l
|
|
@@ -2303,6 +2149,11 @@ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
|
2303
2149
|
_orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
2304
2150
|
if [ "\$_orphan_commits" -gt 0 ]; then
|
|
2305
2151
|
echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
|
|
2152
|
+
# FIX-045: rebase onto origin/main before publishing — avoids BEHIND state on GitHub
|
|
2153
|
+
if ! ( cd "\$_orphan_wt" && git fetch origin main 2>/dev/null && git rebase origin/main 2>/dev/null ); then
|
|
2154
|
+
echo "[loop] FIX-045: orphan \$_orphan_branch rebase failed — skipping recovery (conflict or network error)"
|
|
2155
|
+
continue
|
|
2156
|
+
fi
|
|
2306
2157
|
_orphan_ok=0
|
|
2307
2158
|
if ( cd "\$_orphan_wt" && _loop_is_doc_only_change ); then
|
|
2308
2159
|
( cd "\$_orphan_wt" && _loop_publish_doc_pr "\$_orphan_branch" "doc: recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
@@ -2393,13 +2244,21 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2393
2244
|
_loop_event idle "\${CYCLE_ID}" "" "" || true
|
|
2394
2245
|
echo "[loop] cycle \${CYCLE_ID}: idle (no new commits); worktree cleaned"
|
|
2395
2246
|
else
|
|
2396
|
-
|
|
2247
|
+
_is_doc_only=0
|
|
2248
|
+
( cd "\$WT" && _loop_is_doc_only_change ) && _is_doc_only=1
|
|
2249
|
+
if [ "\$_is_doc_only" -eq 1 ]; then
|
|
2397
2250
|
( cd "\$WT" && _loop_publish_doc_pr "\$BRANCH" "doc: loop cycle \${CYCLE_ID}" )
|
|
2398
2251
|
else
|
|
2399
2252
|
( cd "\$WT" && _loop_publish_pr "\$BRANCH" "loop cycle \${CYCLE_ID}" )
|
|
2400
2253
|
fi
|
|
2401
2254
|
_publish_status=\$?
|
|
2402
2255
|
if [ "\$_publish_status" -eq 0 ]; then
|
|
2256
|
+
# FIX-047: CI green ≠ delivered — wait for actual PR merge before cycle complete
|
|
2257
|
+
if [ "\$_is_doc_only" -eq 0 ]; then
|
|
2258
|
+
if ! ( cd "\$WT" && _loop_wait_pr_merge "\$BRANCH" ); then
|
|
2259
|
+
_worktree_alert "cycle \${CYCLE_ID}: FIX-047: PR not merged within timeout — code may not be in main (BRANCH=\${BRANCH})"
|
|
2260
|
+
fi
|
|
2261
|
+
fi
|
|
2403
2262
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2404
2263
|
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true
|
|
2405
2264
|
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
@@ -2731,8 +2590,10 @@ cmd_loop() {
|
|
|
2731
2590
|
pause) _loop_pause ;;
|
|
2732
2591
|
resume) _loop_resume ;;
|
|
2733
2592
|
reset) _loop_reset ;;
|
|
2734
|
-
notify)
|
|
2735
|
-
|
|
2593
|
+
notify) _notify "${1:-roll}" "${2:-}" ;;
|
|
2594
|
+
enforce-tcr) _loop_enforce_tcr "${1:-}" "${2:-}" ;;
|
|
2595
|
+
precheck-ci) _loop_precheck_ci ;;
|
|
2596
|
+
*) err "Usage: roll loop <on|off|now|test|status|monitor|runs|events|attach|mute|unmute|pause|resume|reset|notify|enforce-tcr|precheck-ci>"; exit 1 ;;
|
|
2736
2597
|
esac
|
|
2737
2598
|
}
|
|
2738
2599
|
|
|
@@ -3251,6 +3112,20 @@ _gh_repo_slug() {
|
|
|
3251
3112
|
printf "%s\n" "$url"
|
|
3252
3113
|
}
|
|
3253
3114
|
|
|
3115
|
+
# Returns 0 if gh CLI is installed and executable, 1 otherwise.
|
|
3116
|
+
_gh_available() { command -v gh >/dev/null 2>&1; }
|
|
3117
|
+
|
|
3118
|
+
# Resolve the GitHub owner/repo slug and set <outvar> to it.
|
|
3119
|
+
# Returns 0 on success. Returns 1 (no output) if gh is unavailable or the
|
|
3120
|
+
# remote is not a GitHub URL — caller decides how to handle failure.
|
|
3121
|
+
_gh_resolve() {
|
|
3122
|
+
local _outvar="$1"
|
|
3123
|
+
_gh_available || return 1
|
|
3124
|
+
local _slug
|
|
3125
|
+
_slug=$(_gh_repo_slug 2>/dev/null) || return 1
|
|
3126
|
+
printf -v "$_outvar" '%s' "$_slug"
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3254
3129
|
# Poll gh run list until current commit's CI completes.
|
|
3255
3130
|
# Returns 0 on success (or when gh binary missing — graceful skip).
|
|
3256
3131
|
# Returns 1 on CI failure, timeout, or any gh call failure.
|
|
@@ -3259,7 +3134,7 @@ _ci_wait() {
|
|
|
3259
3134
|
local interval=15
|
|
3260
3135
|
local elapsed=0
|
|
3261
3136
|
|
|
3262
|
-
|
|
3137
|
+
_gh_available || { warn "gh not installed — skipping CI gate gh 未安装,跳过 CI 检查"; return 0; }
|
|
3263
3138
|
|
|
3264
3139
|
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
|
|
3265
3140
|
local short; short=$(git rev-parse --short HEAD 2>/dev/null)
|
|
@@ -3281,6 +3156,17 @@ _ci_wait() {
|
|
|
3281
3156
|
}
|
|
3282
3157
|
|
|
3283
3158
|
if [[ -z "$runs" || "$runs" == "[]" ]]; then
|
|
3159
|
+
# FIX-046: CI only fires on pull_request events — without a PR, runs will never appear.
|
|
3160
|
+
# Check if an open PR exists; if not, skip the gate gracefully.
|
|
3161
|
+
local _branch; _branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
3162
|
+
if [[ -n "$_branch" ]]; then
|
|
3163
|
+
local _pr_json; _pr_json=$(gh -R "$repo_slug" pr list --head "$_branch" --state open --json number 2>/dev/null || echo "1")
|
|
3164
|
+
local _pr_count; _pr_count=$(echo "$_pr_json" | jq 'length' 2>/dev/null || echo "1")
|
|
3165
|
+
if [[ "$_pr_count" == "0" ]]; then
|
|
3166
|
+
warn "No open PR for ${_branch} — CI not triggered; skipping CI gate 当前分支无 PR,CI 未触发,跳过"
|
|
3167
|
+
return 0
|
|
3168
|
+
fi
|
|
3169
|
+
fi
|
|
3284
3170
|
(( elapsed == 0 )) && echo " No CI runs found yet, waiting... 尚无 CI 记录,等待触发..."
|
|
3285
3171
|
sleep "$interval"
|
|
3286
3172
|
elapsed=$(( elapsed + interval ))
|
|
@@ -3320,10 +3206,9 @@ _ci_wait() {
|
|
|
3320
3206
|
# Returns 0: ok to proceed (green / pending / unknown / no gh).
|
|
3321
3207
|
# Returns 1: HEAD CI is definitively red → ALERT written, do not build.
|
|
3322
3208
|
_loop_precheck_ci() {
|
|
3323
|
-
|
|
3209
|
+
local slug; _gh_resolve slug || return 0
|
|
3324
3210
|
|
|
3325
3211
|
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || return 0
|
|
3326
|
-
local slug; slug=$(_gh_repo_slug) || return 0
|
|
3327
3212
|
|
|
3328
3213
|
local runs
|
|
3329
3214
|
runs=$(gh -R "$slug" run list --commit "$commit" --json conclusion 2>/dev/null) || return 0
|
|
@@ -3458,13 +3343,22 @@ _loop_self_heal_ci() {
|
|
|
3458
3343
|
[[ -z "$story_id" ]] && return 1
|
|
3459
3344
|
[[ "${ROLL_LOOP_NO_HEAL:-0}" == "1" ]] && return 1
|
|
3460
3345
|
local max="${ROLL_LOOP_HEAL_MAX:-2}"
|
|
3461
|
-
local
|
|
3462
|
-
local
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3346
|
+
local state="${_LOOP_STATE:-${HOME}/.shared/roll/loop/state.yaml}"
|
|
3347
|
+
local current=0
|
|
3348
|
+
if [[ -f "$state" ]]; then
|
|
3349
|
+
local raw; raw=$(grep '^heal_count:' "$state" 2>/dev/null | awk '{print $2}')
|
|
3350
|
+
[[ "$raw" =~ ^[0-9]+$ ]] && current="$raw"
|
|
3351
|
+
fi
|
|
3466
3352
|
[[ "$current" -ge "$max" ]] && return 1
|
|
3467
|
-
|
|
3353
|
+
local new=$((current + 1))
|
|
3354
|
+
mkdir -p "$(dirname "$state")"
|
|
3355
|
+
if grep -q '^heal_count:' "$state" 2>/dev/null; then
|
|
3356
|
+
local tmp; tmp=$(mktemp)
|
|
3357
|
+
sed "s/^heal_count:.*/heal_count: ${new}/" "$state" > "$tmp"
|
|
3358
|
+
mv "$tmp" "$state"
|
|
3359
|
+
else
|
|
3360
|
+
echo "heal_count: ${new}" >> "$state"
|
|
3361
|
+
fi
|
|
3468
3362
|
return 0
|
|
3469
3363
|
}
|
|
3470
3364
|
|
|
@@ -3473,7 +3367,11 @@ _loop_self_heal_ci() {
|
|
|
3473
3367
|
_loop_clear_heal_state() {
|
|
3474
3368
|
local story_id="$1"
|
|
3475
3369
|
[[ -z "$story_id" ]] && return 0
|
|
3476
|
-
|
|
3370
|
+
local state="${_LOOP_STATE:-${HOME}/.shared/roll/loop/state.yaml}"
|
|
3371
|
+
[[ ! -f "$state" ]] && return 0
|
|
3372
|
+
local tmp; tmp=$(mktemp)
|
|
3373
|
+
grep -v '^heal_count:' "$state" > "$tmp"
|
|
3374
|
+
mv "$tmp" "$state"
|
|
3477
3375
|
return 0
|
|
3478
3376
|
}
|
|
3479
3377
|
|
|
@@ -3746,7 +3644,7 @@ _loop_pr_rebase_stale() {
|
|
|
3746
3644
|
local pr="$1" head_ref="$2"
|
|
3747
3645
|
[ -n "$pr" ] && [ -n "$head_ref" ] || return 0
|
|
3748
3646
|
|
|
3749
|
-
local slug; slug
|
|
3647
|
+
local slug; _gh_resolve slug || return 0
|
|
3750
3648
|
|
|
3751
3649
|
local pr_json
|
|
3752
3650
|
pr_json=$(gh -R "$slug" pr view "$pr" --json headRepository,headRepositoryOwner,isCrossRepository 2>/dev/null) || return 0
|
|
@@ -3780,9 +3678,7 @@ _loop_pr_rebase_stale() {
|
|
|
3780
3678
|
# Walks open PRs and routes each by classification.
|
|
3781
3679
|
# Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
|
|
3782
3680
|
_loop_pr_inbox() {
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
local slug; slug=$(_gh_repo_slug 2>/dev/null) || return 0
|
|
3681
|
+
local slug; _gh_resolve slug || return 0
|
|
3786
3682
|
local prs_json
|
|
3787
3683
|
prs_json=$(gh -R "$slug" pr list --state open \
|
|
3788
3684
|
--json number,headRefName,author,title \
|
|
@@ -4219,15 +4115,10 @@ _loop_cleanup_stale_cycle_branches() {
|
|
|
4219
4115
|
_loop_publish_pr() {
|
|
4220
4116
|
local branch="$1"
|
|
4221
4117
|
local title="${2:-loop cycle ${branch#loop/}}"
|
|
4222
|
-
|
|
4223
|
-
_worktree_alert "_loop_publish_pr: gh not installed; cannot publish PR for ${branch}"
|
|
4118
|
+
local slug; _gh_resolve slug || {
|
|
4119
|
+
_worktree_alert "_loop_publish_pr: gh not installed or origin is not a github repo; cannot publish PR for ${branch}"
|
|
4224
4120
|
return 2
|
|
4225
|
-
|
|
4226
|
-
local slug; slug=$(_gh_repo_slug 2>/dev/null) || slug=""
|
|
4227
|
-
if [ -z "$slug" ]; then
|
|
4228
|
-
_worktree_alert "_loop_publish_pr: origin remote is not a github repo; cannot publish PR for ${branch}"
|
|
4229
|
-
return 2
|
|
4230
|
-
fi
|
|
4121
|
+
}
|
|
4231
4122
|
local _push_err
|
|
4232
4123
|
_push_err=$(git push origin "$branch" 2>&1) || {
|
|
4233
4124
|
_worktree_alert "_loop_publish_pr: push origin ${branch} failed: ${_push_err}"
|
|
@@ -4253,6 +4144,29 @@ _loop_publish_pr() {
|
|
|
4253
4144
|
return 0
|
|
4254
4145
|
}
|
|
4255
4146
|
|
|
4147
|
+
# _loop_wait_pr_merge <branch>
|
|
4148
|
+
# FIX-047: poll GitHub until PR for <branch> is merged (confirms delivery).
|
|
4149
|
+
# Returns 0: merged. Returns 1: CLOSED or timeout.
|
|
4150
|
+
# Gracefully skips (returns 0) when gh is unavailable or slug unparseable.
|
|
4151
|
+
# Timeout: ROLL_PR_MERGE_TIMEOUT (default 600s).
|
|
4152
|
+
_loop_wait_pr_merge() {
|
|
4153
|
+
local branch="$1"
|
|
4154
|
+
local timeout="${ROLL_PR_MERGE_TIMEOUT:-600}"
|
|
4155
|
+
local interval=30
|
|
4156
|
+
local elapsed=0
|
|
4157
|
+
local slug; _gh_resolve slug || return 0
|
|
4158
|
+
while (( elapsed < timeout )); do
|
|
4159
|
+
local state; state=$(gh -R "$slug" pr view "$branch" --json state -q .state 2>/dev/null || echo "UNKNOWN")
|
|
4160
|
+
case "$state" in
|
|
4161
|
+
MERGED) return 0 ;;
|
|
4162
|
+
CLOSED) return 1 ;;
|
|
4163
|
+
esac
|
|
4164
|
+
sleep "$interval"
|
|
4165
|
+
elapsed=$(( elapsed + interval ))
|
|
4166
|
+
done
|
|
4167
|
+
return 1
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4256
4170
|
# _loop_is_doc_only_change
|
|
4257
4171
|
# Returns 0 if every file changed since origin/main is doc-only
|
|
4258
4172
|
# (BACKLOG.md, CHANGELOG.md, PROPOSALS.md, docs/, .claude/).
|
|
@@ -4271,15 +4185,10 @@ _loop_is_doc_only_change() {
|
|
|
4271
4185
|
_loop_publish_doc_pr() {
|
|
4272
4186
|
local branch="$1"
|
|
4273
4187
|
local title="${2:-doc update ${branch#loop/}}"
|
|
4274
|
-
|
|
4275
|
-
_worktree_alert "_loop_publish_doc_pr: gh not installed; cannot publish PR for ${branch}"
|
|
4188
|
+
local slug; _gh_resolve slug || {
|
|
4189
|
+
_worktree_alert "_loop_publish_doc_pr: gh not installed or origin is not a github repo; cannot publish PR for ${branch}"
|
|
4276
4190
|
return 2
|
|
4277
|
-
|
|
4278
|
-
local slug; slug=$(_gh_repo_slug 2>/dev/null) || slug=""
|
|
4279
|
-
if [ -z "$slug" ]; then
|
|
4280
|
-
_worktree_alert "_loop_publish_doc_pr: origin remote is not a github repo; cannot publish PR for ${branch}"
|
|
4281
|
-
return 2
|
|
4282
|
-
fi
|
|
4191
|
+
}
|
|
4283
4192
|
if ! git push origin "$branch" --quiet 2>/dev/null; then
|
|
4284
4193
|
_worktree_alert "_loop_publish_doc_pr: push origin ${branch} failed"
|
|
4285
4194
|
return 1
|
|
@@ -4657,7 +4566,7 @@ cmd_ci() {
|
|
|
4657
4566
|
return
|
|
4658
4567
|
fi
|
|
4659
4568
|
|
|
4660
|
-
|
|
4569
|
+
_gh_available || { warn "gh not installed gh 未安装"; return 0; }
|
|
4661
4570
|
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
|
|
4662
4571
|
local runs
|
|
4663
4572
|
runs=$(gh run list --commit "$commit" --json status,conclusion,name 2>/dev/null) || { warn "gh run list failed"; return 0; }
|
|
@@ -4962,7 +4871,7 @@ _dash_ac_completion() {
|
|
|
4962
4871
|
# ④ DoD CI signal — query gh for HEAD's most-recent run conclusion.
|
|
4963
4872
|
# Returns: success | pending | failure | none
|
|
4964
4873
|
_dash_ci_status() {
|
|
4965
|
-
|
|
4874
|
+
_gh_available || { echo "none"; return; }
|
|
4966
4875
|
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { echo "none"; return; }
|
|
4967
4876
|
local slug; slug=$(_gh_repo_slug 2>/dev/null) || true
|
|
4968
4877
|
local out
|
package/lib/loop-fmt.py
CHANGED
|
@@ -9,15 +9,77 @@ Tier 1 (signal): tcr commit, story skill, peer verdict, ci gate, pr merge, e
|
|
|
9
9
|
import sys
|
|
10
10
|
import json
|
|
11
11
|
import re
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
12
15
|
from datetime import datetime, timezone
|
|
13
16
|
|
|
17
|
+
_SPIN_ENABLED = os.environ.get("LOOP_FMT_NO_SPIN", "0") != "1"
|
|
18
|
+
SPIN_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
19
|
+
|
|
14
20
|
DARK_GRAY = "\033[90m"
|
|
15
21
|
CYAN = "\033[36m"
|
|
16
22
|
WHITE = "\033[97m"
|
|
17
23
|
GREEN = "\033[32m"
|
|
18
24
|
RED = "\033[31m"
|
|
25
|
+
YELLOW = "\033[33m"
|
|
19
26
|
RESET = "\033[0m"
|
|
20
27
|
|
|
28
|
+
|
|
29
|
+
class Spinner:
|
|
30
|
+
"""Animated wait indicator for long-running operations.
|
|
31
|
+
|
|
32
|
+
In production (LOOP_FMT_NO_SPIN=0): background thread writes frames using \\r.
|
|
33
|
+
In test mode (LOOP_FMT_NO_SPIN=1): writes a static ⏳ line to stdout instead.
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._thread = None
|
|
37
|
+
self._running = False
|
|
38
|
+
self._label = ""
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def active(self):
|
|
43
|
+
return self._running
|
|
44
|
+
|
|
45
|
+
def start(self, label):
|
|
46
|
+
with self._lock:
|
|
47
|
+
if self._running:
|
|
48
|
+
self._label = label # update without restart
|
|
49
|
+
return
|
|
50
|
+
self._label = label
|
|
51
|
+
self._running = True
|
|
52
|
+
if _SPIN_ENABLED:
|
|
53
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
54
|
+
self._thread.start()
|
|
55
|
+
else:
|
|
56
|
+
sys.stdout.write(f" {YELLOW}⏳ {label}...{RESET}\n")
|
|
57
|
+
sys.stdout.flush()
|
|
58
|
+
|
|
59
|
+
def stop(self):
|
|
60
|
+
with self._lock:
|
|
61
|
+
was_running = self._running
|
|
62
|
+
self._running = False
|
|
63
|
+
if self._thread:
|
|
64
|
+
self._thread.join(timeout=0.3)
|
|
65
|
+
self._thread = None
|
|
66
|
+
if _SPIN_ENABLED and was_running:
|
|
67
|
+
sys.stdout.write(f"\r{' ' * 60}\r")
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
|
|
70
|
+
def _run(self):
|
|
71
|
+
i = 0
|
|
72
|
+
while self._running:
|
|
73
|
+
with self._lock:
|
|
74
|
+
label = self._label
|
|
75
|
+
frame = SPIN_FRAMES[i % len(SPIN_FRAMES)]
|
|
76
|
+
sys.stdout.write(f"\r {YELLOW}{frame} {label}...{RESET}")
|
|
77
|
+
sys.stdout.flush()
|
|
78
|
+
time.sleep(0.12)
|
|
79
|
+
i += 1
|
|
80
|
+
sys.stdout.write(f"\r{' ' * 60}\r")
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
|
|
21
83
|
SUPPRESS_TOOLS = {"Read", "Glob", "Grep", "ReadMcpResourceTool", "ListMcpResourcesTool",
|
|
22
84
|
"WebFetch", "WebSearch", "TaskCreate", "TaskGet", "TaskList",
|
|
23
85
|
"TaskUpdate", "TaskOutput", "TaskStop"}
|
|
@@ -45,13 +107,15 @@ def stamp(text, muted=False):
|
|
|
45
107
|
|
|
46
108
|
class LoopFmt:
|
|
47
109
|
def __init__(self):
|
|
48
|
-
self.last_bash_cmd
|
|
49
|
-
self.tcr_count
|
|
110
|
+
self.last_bash_cmd = ""
|
|
111
|
+
self.tcr_count = 0
|
|
50
112
|
self.last_test_count = None
|
|
51
|
-
self.cycle_num
|
|
52
|
-
self.pending_commit
|
|
53
|
-
self.pending_pr
|
|
54
|
-
self.pending_ci
|
|
113
|
+
self.cycle_num = None
|
|
114
|
+
self.pending_commit = False
|
|
115
|
+
self.pending_pr = False
|
|
116
|
+
self.pending_ci = False
|
|
117
|
+
self.pending_story = False
|
|
118
|
+
self.spinner = Spinner()
|
|
55
119
|
|
|
56
120
|
def _extract_cycle_num(self, text):
|
|
57
121
|
m = re.search(r'cycle[#\s]+(\d+)', text, re.IGNORECASE)
|
|
@@ -139,8 +203,10 @@ class LoopFmt:
|
|
|
139
203
|
self.pending_commit = True
|
|
140
204
|
elif re.search(r'gh pr (create|merge)', cmd):
|
|
141
205
|
self.pending_pr = True
|
|
142
|
-
|
|
206
|
+
self.spinner.start("merging PR")
|
|
207
|
+
elif re.search(r'(roll ci|npm run ci|_ci_wait|ci:local)', cmd):
|
|
143
208
|
self.pending_ci = True
|
|
209
|
+
self.spinner.start("waiting for CI")
|
|
144
210
|
return # Wait for result
|
|
145
211
|
|
|
146
212
|
if name == "Skill":
|
|
@@ -151,6 +217,8 @@ class LoopFmt:
|
|
|
151
217
|
print()
|
|
152
218
|
print(stamp(f"cycle #{self.cycle_num or '?'} — picking story"))
|
|
153
219
|
print(step("story", us_id, trunc(args, 60)))
|
|
220
|
+
self.pending_story = True
|
|
221
|
+
self.spinner.start("executing story")
|
|
154
222
|
return
|
|
155
223
|
|
|
156
224
|
# All other tools (Agent, ToolSearch, etc.): suppress
|
|
@@ -189,7 +257,13 @@ class LoopFmt:
|
|
|
189
257
|
print(step("tcr", commit_hash, f"{commit_msg}{test_part}"))
|
|
190
258
|
return
|
|
191
259
|
|
|
260
|
+
if self.pending_story:
|
|
261
|
+
self.pending_story = False
|
|
262
|
+
self.spinner.stop()
|
|
263
|
+
return # story result content suppressed; TCR events showed the work
|
|
264
|
+
|
|
192
265
|
if self.pending_pr:
|
|
266
|
+
self.spinner.stop()
|
|
193
267
|
self.pending_pr = False
|
|
194
268
|
m = re.search(r'#(\d+)', text)
|
|
195
269
|
if m:
|
|
@@ -201,6 +275,7 @@ class LoopFmt:
|
|
|
201
275
|
return
|
|
202
276
|
|
|
203
277
|
if self.pending_ci:
|
|
278
|
+
self.spinner.stop()
|
|
204
279
|
self.pending_ci = False
|
|
205
280
|
has_green = re.search(r'(green|pass|success|all tests)', text, re.IGNORECASE)
|
|
206
281
|
has_red = re.search(r'(red|fail|error)', text, re.IGNORECASE)
|
package/package.json
CHANGED
|
@@ -416,6 +416,10 @@ prompt 会包含:
|
|
|
416
416
|
(即使没有 deep doc 也要列)
|
|
417
417
|
- Feature 名跟 `docs/features/<file>.md` 文件名一致时,加链接到该 md
|
|
418
418
|
- 没有对应 deep doc 的 Feature,**只写 plain text 不加链接**
|
|
419
|
+
- **Planning distinction(US-DOC-011)**:
|
|
420
|
+
- 该 Feature 下**所有** Story 均为 `📋 Todo` → 在描述末尾追加 `*(规划中)*`
|
|
421
|
+
- 只要有 **≥1 个** `✅ Done` Story → 正常展示,**不加**任何标记
|
|
422
|
+
- 一眼可见:规划中的 Feature 在每个 Epic 分组的末尾列出
|
|
419
423
|
- 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
|
|
420
424
|
- 分组用 BACKLOG 的 Epic 名,原序,不重排
|
|
421
425
|
- Core Highlights 从所有 Features 里挑 3-5 个最能代表产品定位的,
|
|
@@ -45,7 +45,9 @@ $roll-.review unstaged
|
|
|
45
45
|
$roll-.review files src/utils.ts
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
## Review Dimensions (
|
|
48
|
+
## Review Dimensions (7 Core Dimensions)
|
|
49
|
+
|
|
50
|
+
Original 6 dimensions plus Reuse (added in REFACTOR-022, simplify three-axis integration):
|
|
49
51
|
|
|
50
52
|
```
|
|
51
53
|
┌─────────────────────────────────────────────────────────┐
|
|
@@ -54,13 +56,42 @@ $roll-.review files src/utils.ts
|
|
|
54
56
|
│ ✅ Correctness - Logic is correct, no bugs │
|
|
55
57
|
│ ✅ Security - No vulnerabilities, input valid. │
|
|
56
58
|
│ ✅ Maintainability - Clear naming, sound structure │
|
|
59
|
+
│ Quality anti-patterns (check each): │
|
|
60
|
+
│ □ Redundant state / cached values that could be │
|
|
61
|
+
│ derived directly │
|
|
62
|
+
│ □ Parameter sprawl — new param vs. restructure │
|
|
63
|
+
│ □ Copy-paste with slight variation (near-dup) │
|
|
64
|
+
│ □ Leaky abstraction — exposes internal details │
|
|
65
|
+
│ □ Stringly-typed — raw string where constant │
|
|
66
|
+
│ / enum exists │
|
|
67
|
+
│ □ Unnecessary JSX nesting (no layout value) │
|
|
68
|
+
│ □ Nested conditionals ≥3 deep (ternary chains, │
|
|
69
|
+
│ nested if/else) — flatten with early return │
|
|
70
|
+
│ □ Unnecessary comments explaining WHAT │
|
|
57
71
|
│ ✅ Performance - No performance pitfalls │
|
|
72
|
+
│ Efficiency anti-patterns (check each): │
|
|
73
|
+
│ □ Redundant computation / repeated file read / │
|
|
74
|
+
│ duplicate API call / N+1 pattern │
|
|
75
|
+
│ □ Missed concurrency — independent ops sequential │
|
|
76
|
+
│ □ Hot-path bloat — blocking work in startup or │
|
|
77
|
+
│ per-request path │
|
|
78
|
+
│ □ Loop no-op updates — missing change-detection │
|
|
79
|
+
│ guard │
|
|
80
|
+
│ □ TOCTOU existence pre-check — operate directly + │
|
|
81
|
+
│ handle error instead │
|
|
82
|
+
│ □ Memory — unbounded structures / missing cleanup │
|
|
83
|
+
│ □ Overly broad op — reading full file for a slice │
|
|
58
84
|
│ ✅ Testability - Easy to test, edge cases covered │
|
|
59
85
|
│ ✅ Scope - Focused on current task, no │
|
|
60
86
|
│ unrelated changes │
|
|
87
|
+
│ ✅ Reuse - No new code duplicating existing │
|
|
88
|
+
│ □ New function duplicates existing utility/helper │
|
|
89
|
+
│ □ Inline logic replaceable by existing tool │
|
|
61
90
|
└─────────────────────────────────────────────────────────┘
|
|
62
91
|
```
|
|
63
92
|
|
|
93
|
+
**Usage in TCR**: Each micro-step review is a lightweight self-check against this checklist — no sub-agents, zero extra token cost. The three-axis deep review with parallel agents runs once per Story in `$roll-build` Phase 7.
|
|
94
|
+
|
|
64
95
|
## Severity Levels and Decisions
|
|
65
96
|
|
|
66
97
|
| Level | Definition | Decision |
|
|
@@ -378,33 +378,51 @@ EOF
|
|
|
378
378
|
chmod +x .git/hooks/pre-push
|
|
379
379
|
```
|
|
380
380
|
|
|
381
|
-
### Phase 7: Pre-Push Code Review
|
|
381
|
+
### Phase 7: Pre-Push Code Review (Three-Axis Deep Review)
|
|
382
|
+
|
|
383
|
+
This phase runs **once per Story** (not per micro-step) on the full accumulated diff.
|
|
384
|
+
Per-micro-step review uses `$roll-.review staged` inline checklist (zero extra cost).
|
|
385
|
+
|
|
386
|
+
**Phase 3.5 vs Phase 7 split**: Phase 3.5 (Peer Review) focuses on architectural direction
|
|
387
|
+
and approach before coding begins. Phase 7 focuses on implementation quality after all
|
|
388
|
+
micro-steps are done — catching issues that only appear at diff scale (parameter sprawl
|
|
389
|
+
across files, copy-paste patterns, cross-file N+1, etc.).
|
|
382
390
|
|
|
383
391
|
```bash
|
|
384
|
-
|
|
392
|
+
# Capture full Story diff
|
|
393
|
+
git diff main...HEAD
|
|
385
394
|
```
|
|
386
395
|
|
|
387
|
-
**
|
|
396
|
+
**Launch three review agents in parallel** (each receives the full diff):
|
|
397
|
+
|
|
388
398
|
```
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
Agent 1: Reuse Review
|
|
400
|
+
→ Search for existing utilities / helpers the new code could use instead
|
|
401
|
+
→ Flag any new function that duplicates existing functionality
|
|
402
|
+
→ Flag inline logic replaceable by existing tools
|
|
403
|
+
|
|
404
|
+
Agent 2: Quality Review
|
|
405
|
+
→ Redundant state, Parameter sprawl, Copy-paste near-duplicate,
|
|
406
|
+
Leaky abstraction, Stringly-typed, JSX nesting,
|
|
407
|
+
Nested conditionals ≥3 deep, Unnecessary comments
|
|
408
|
+
|
|
409
|
+
Agent 3: Efficiency Review
|
|
410
|
+
→ Redundant computation / N+1, Missed concurrency,
|
|
411
|
+
Hot-path bloat, Loop no-op updates, TOCTOU existence pre-check,
|
|
412
|
+
Memory leaks, Overly broad operations
|
|
395
413
|
```
|
|
396
414
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
-
|
|
415
|
+
Wait for all three agents to complete. Aggregate findings → fix each issue
|
|
416
|
+
(false positives: note and skip, no debate) → summarize what was fixed.
|
|
417
|
+
|
|
418
|
+
**Fallback**: If parallel agent invocation fails, run `$roll-.review staged` on
|
|
419
|
+
the full diff as a single-pass fallback — do not skip review entirely.
|
|
402
420
|
|
|
403
421
|
**Decision:**
|
|
404
422
|
```
|
|
405
423
|
🔴 Critical > 0 → Fix via new TCR cycle → Re-review
|
|
406
424
|
🟡 Warnings > 0 → Fix if quick (< 5 min) or document
|
|
407
|
-
🟢 Suggestions / ✅ All clear → Proceed to
|
|
425
|
+
🟢 Suggestions / ✅ All clear → Proceed to Phase 8
|
|
408
426
|
```
|
|
409
427
|
|
|
410
428
|
### Phase 8: Commit & Push
|
|
@@ -96,7 +96,7 @@ fi
|
|
|
96
96
|
|
|
97
97
|
### Step 1.5 — Pre-run CI Health Check
|
|
98
98
|
|
|
99
|
-
Call `
|
|
99
|
+
Call `roll loop precheck-ci` before scanning BACKLOG. This is a **defensive gate**
|
|
100
100
|
against building on a broken base — if the most recent commit on the branch
|
|
101
101
|
has red CI, the loop must not stack new commits on top (which would create the
|
|
102
102
|
exact stuck-red state FIX-026 traces to).
|
|
@@ -105,7 +105,7 @@ exact stuck-red state FIX-026 traces to).
|
|
|
105
105
|
- HEAD CI red → write ALERT, **do not pick up any stories this cycle**,
|
|
106
106
|
exit cleanly. The next cycle will retry; the human must fix CI manually
|
|
107
107
|
(typically by reverting or pushing a green commit) before the loop resumes.
|
|
108
|
-
- `gh` missing or repo unparseable → graceful skip (`
|
|
108
|
+
- `gh` missing or repo unparseable → graceful skip (`roll loop precheck-ci`
|
|
109
109
|
returns 0); the post-build `_loop_enforce_ci` remains the strict gate.
|
|
110
110
|
|
|
111
111
|
### Step 1.6 — PR Inbox (US-AUTO-034)
|
|
@@ -229,7 +229,7 @@ run_id: loop-20260510-0200
|
|
|
229
229
|
|
|
230
230
|
After each item completes:
|
|
231
231
|
|
|
232
|
-
1. **TCR 硬校验** — call `
|
|
232
|
+
1. **TCR 硬校验** — call `roll loop enforce-tcr <story_id> <started_at>`:
|
|
233
233
|
- Count `tcr:` prefix commits since `started_at` via `git log --oneline --since=<started_at>`
|
|
234
234
|
- Count == 0 → revert story status in BACKLOG.md from ✅ Done → 📋 Todo; write ALERT to `~/.shared/roll/loop/ALERT.md` with story ID, time, reason "zero tcr: commits since story start", and suggested actions (`roll loop now` / `$roll-build <id>` / `roll loop reset`)
|
|
235
235
|
- Count > 0 → continue normally
|
|
@@ -246,34 +246,36 @@ After each item completes:
|
|
|
246
246
|
(return 0). Any other `gh` error is **not** "gh unavailable" — it is a
|
|
247
247
|
hard failure and must block the gate.
|
|
248
248
|
|
|
249
|
-
**CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
249
|
+
**CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT.
|
|
250
|
+
|
|
251
|
+
Call `_loop_self_heal_ci <story_id>` to check if another attempt is permitted.
|
|
252
|
+
|
|
253
|
+
**Path A — attempt allowed (exit 0, counter incremented in `state.yaml`):**
|
|
254
|
+
|
|
255
|
+
1. Capture failure summary:
|
|
256
|
+
```
|
|
257
|
+
gh run view --log-failed --repo <slug> \
|
|
258
|
+
$(gh run list --commit HEAD --json databaseId,conclusion -L 5 \
|
|
259
|
+
| jq -r '.[] | select(.conclusion=="failure") | .databaseId' | head -1) \
|
|
260
|
+
2>/dev/null | head -200 > /tmp/roll-heal-<story_id>.log
|
|
261
|
+
```
|
|
262
|
+
2. Invoke `Skill("roll-fix")` with brief:
|
|
263
|
+
`"CI red after <story_id>. Failing run logs at /tmp/roll-heal-<story_id>.log.
|
|
264
|
+
Diagnose root cause, fix via TCR, commit, push. Do NOT change <story_id>'s
|
|
265
|
+
BACKLOG status — it stays ✅ Done. The fix is a follow-up."`
|
|
266
|
+
3. After `roll-fix` completes, return to step 2 (CI Gate) — re-run `roll ci --wait`.
|
|
267
|
+
The counter in `state.yaml` prevents infinite loops.
|
|
268
|
+
|
|
269
|
+
**Path B — heal exhausted (≥`ROLL_LOOP_HEAL_MAX`, default 2) or disabled (`ROLL_LOOP_NO_HEAL=1`) (exit 1):**
|
|
270
|
+
|
|
271
|
+
1. Keep story as ✅ Done — commits are already on main; CI red is a follow-up
|
|
272
|
+
problem, not a story failure.
|
|
273
|
+
2. Write ALERT to `~/.shared/roll/loop/ALERT.md` with:
|
|
274
|
+
- story ID, time, commit SHA
|
|
275
|
+
- heal attempts made (read `heal_count:` from `state.yaml`)
|
|
276
|
+
- last failure summary (head of `/tmp/roll-heal-<story_id>.log`)
|
|
277
|
+
- suggested actions: `$roll-fix` manually / inspect CI / `roll loop reset`
|
|
278
|
+
3. Skip to next story.
|
|
277
279
|
|
|
278
280
|
**Bypass for debugging / cost control:** set `ROLL_LOOP_NO_HEAL=1` to restore
|
|
279
281
|
pre-US-AUTO-041 fail-fast behaviour.
|