@seanyao/roll 2026.514.2 → 2026.514.5
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 +22 -5
- package/README.md +12 -17
- package/bin/roll +358 -5
- package/conventions/global/AGENTS.md +10 -0
- package/conventions/templates/backend-service/AGENTS.md +5 -0
- package/conventions/templates/cli/AGENTS.md +5 -0
- package/conventions/templates/frontend-only/AGENTS.md +5 -0
- package/conventions/templates/fullstack/AGENTS.md +5 -0
- package/package.json +1 -1
- package/skills/roll-design/SKILL.md +23 -0
- package/skills/roll-doc/SKILL.md +23 -0
- package/skills/roll-loop/SKILL.md +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v2026.514.
|
|
3
|
+
## v2026.514.5
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- **Fixed**: 上版 `claude/*` 临时分支清理意外失效 — 现已恢复 `[loop]`
|
|
6
|
+
- **Fixed**: loop session 结束后本地 worktree 不再积累,`git worktree list` 保持干净 `[loop]`
|
|
7
|
+
- **Fixed**: 发版脚本不再维护独立的 agent 检测逻辑,配置变更时两处不再悄悄漂移
|
|
8
|
+
|
|
9
|
+
## v2026.514.3
|
|
10
|
+
|
|
11
|
+
### 约定与导航
|
|
12
|
+
|
|
13
|
+
- `$roll-design` 澄清需求前先自己定位产品端和业务域,问你的问题少了
|
|
14
|
+
- `$roll-doc` 为已有项目生成 AGENTS.md 导航骨架 — 新接入 Roll 不再从空白出发
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
### 自动化流水线
|
|
17
|
+
|
|
18
|
+
- loop 每轮先消化开放 PR 再领新 backlog — 把队列里的 PR 当成首类工作,不是绕开的障碍 `[loop]`
|
|
19
|
+
- 自己开的 `loop/*` 分支不会被自己评审,避免同源 bias `[loop]`
|
|
20
|
+
- 24 小时内 rebase 同一个 PR 超过 3 次自动熔断,workflow 文件出错时不再无限循环 `[loop]`
|
|
21
|
+
- 每次 session 收尾自动删掉自己推上去的 `claude/*` 临时分支,远端不再积压"看起来要发 PR 实际不会发"的孤儿分支 `[loop]`
|
|
22
|
+
|
|
23
|
+
## v2026.514.2
|
|
10
24
|
|
|
11
25
|
### 自动化流水线
|
|
12
26
|
|
|
@@ -18,6 +32,8 @@
|
|
|
18
32
|
|
|
19
33
|
- 生成时自动过滤技术黑话,并对照历史风格保持表达一致 `[loop]`
|
|
20
34
|
- 写入前有一道自审:行文不达标就退回重写,不进 changelog `[loop]`
|
|
35
|
+
- 历史版本全部重新整理:按主题分组、合并同类项、附 `[loop]` / `[dream]` 归因标记
|
|
36
|
+
- Release Notes 生成规范写入 Skill:分组规则、条目合并、归因标签、措辞原则
|
|
21
37
|
|
|
22
38
|
### 可见性
|
|
23
39
|
|
|
@@ -28,6 +44,7 @@
|
|
|
28
44
|
### 其他
|
|
29
45
|
|
|
30
46
|
- 纯文档改动直接合 main,不等 CI,合并更快
|
|
47
|
+
- README 新增 Evolution 章节,梳理 Roll 从工具到自主交付系统的演进脉络
|
|
31
48
|
|
|
32
49
|
## v2026.513.1
|
|
33
50
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> _Agents, roll out._
|
|
11
11
|
|
|
12
12
|
**[中文版 README](README_CN.md)**
|
|
13
13
|
|
|
@@ -27,6 +27,16 @@ Roll is an autonomous delivery system for software teams — AI agents pick stor
|
|
|
27
27
|
|
|
28
28
|
_Works with Claude, Cursor, Codex, or your own agent._
|
|
29
29
|
|
|
30
|
+
## Evolution
|
|
31
|
+
|
|
32
|
+
Roll didn't start as a framework. It started as a question: *what if the AI didn't just write code, but actually shipped it?*
|
|
33
|
+
|
|
34
|
+
Early versions just pushed engineering conventions to whichever AI tool you were running. Then came multi-agent support — Kimi, DeepSeek, Codex, Trae — and `roll-peer`, which let one AI challenge another's decisions before anything landed on `main`.
|
|
35
|
+
|
|
36
|
+
The real shift was `roll loop`: stories running back-to-back without human prompting, `roll-.dream` filing its own refactor tickets after nightly scans, the system generating its own work queue. What followed was building enough trust to leave it running overnight — worktree isolation, CI + AI review double gates, real-time visibility into what the agent was actually doing.
|
|
37
|
+
|
|
38
|
+
The goal from here: full delivery, end to end — with humans on the loop, not in it.
|
|
39
|
+
|
|
30
40
|
---
|
|
31
41
|
|
|
32
42
|
## Quick Start (30 seconds)
|
|
@@ -52,6 +62,7 @@ roll loop on # optional: let the agent work unattended
|
|
|
52
62
|
| Loop (autonomous executor) | [guide/en/loop.md](docs/guide/en/loop.md) | [guide/zh/loop.md](docs/guide/zh/loop.md) |
|
|
53
63
|
| Dream (nightly health scan) | [guide/en/dream.md](docs/guide/en/dream.md) | [guide/zh/dream.md](docs/guide/zh/dream.md) |
|
|
54
64
|
| Peer (cross-agent review) | [guide/en/peer.md](docs/guide/en/peer.md) | [guide/zh/peer.md](docs/guide/zh/peer.md) |
|
|
65
|
+
| Configuration (env vars) | [guide/en/configuration.md](docs/guide/en/configuration.md) | [guide/zh/configuration.md](docs/guide/zh/configuration.md) |
|
|
55
66
|
| Skill selection guide | [guide/en/skills.md](docs/guide/en/skills.md) | [guide/zh/skills.md](docs/guide/zh/skills.md) |
|
|
56
67
|
| Domain model (DDD) | [domain/context-map.md](docs/domain/context-map.md) | — |
|
|
57
68
|
| Engineering common sense | [practices/engineering-common-sense.md](docs/practices/engineering-common-sense.md) | — |
|
|
@@ -74,22 +85,6 @@ roll loop on # optional: let the agent work unattended
|
|
|
74
85
|
|
|
75
86
|
---
|
|
76
87
|
|
|
77
|
-
## Evolution
|
|
78
|
-
|
|
79
|
-
Roll didn't start as a framework. It started as a question: *what if the AI didn't just write code, but actually shipped it?*
|
|
80
|
-
|
|
81
|
-
The first version was almost embarrassingly small — a way to push engineering conventions to whatever AI tool you happened to be running. But it needed to be trustworthy before it could be useful, so the early weeks went into making Roll self-maintaining: one-command releases, self-updating installs, a clean npm presence.
|
|
82
|
-
|
|
83
|
-
The next step was making Roll genuinely multi-agent. Kimi, DeepSeek, Codex, Trae — each integrated with its own skill preferences and model bindings. `roll-peer` came from a simple insight: agents shouldn't just review their own work. Have one AI challenge another's design decisions before anything lands on `main`. That turned out to be the first glimpse of what Roll was actually becoming.
|
|
84
|
-
|
|
85
|
-
The real shift happened when `roll loop` went live. Stories started running back-to-back without any human prompt. `roll-.dream` began filing its own refactor tickets after nightly scans. The system had started generating its own work queue — not just executing tasks, but surfacing the next ones.
|
|
86
|
-
|
|
87
|
-
What followed was learning to trust that autonomy: real-time terminal windows, worktree isolation so loop runs never touch your in-progress work, a CI + AI review double gate so nothing merges until it's actually ready. The kind of loop you can leave running overnight and wake up to something mergeable.
|
|
88
|
-
|
|
89
|
-
The goal from here: full delivery, end to end — with humans on the loop, not in it.
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
88
|
## Contributing
|
|
94
89
|
|
|
95
90
|
PRs welcome. Keep them focused on one thing. For larger changes, open an issue first.
|
package/bin/roll
CHANGED
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Roll — AI Agent Convention Manager
|
|
5
5
|
# Single source of truth for how all AI coding agents behave.
|
|
6
6
|
|
|
7
|
-
VERSION="2026.514.
|
|
7
|
+
VERSION="2026.514.5"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -465,7 +465,9 @@ _link_skills() {
|
|
|
465
465
|
local current_target
|
|
466
466
|
current_target="$(readlink "$skill_link")"
|
|
467
467
|
if [[ "$current_target" != "$skill_dir" ]]; then
|
|
468
|
-
ln -sf
|
|
468
|
+
# macOS ln -sf follows symlinks-to-dirs and creates inside instead of
|
|
469
|
+
# replacing — explicitly remove first to guarantee replacement.
|
|
470
|
+
rm -f "$skill_link" && ln -s "$skill_dir" "$skill_link"
|
|
469
471
|
repaired=$((repaired + 1))
|
|
470
472
|
fi
|
|
471
473
|
# correct symlink: skip silently
|
|
@@ -1903,7 +1905,7 @@ _agent_run_skill() {
|
|
|
1903
1905
|
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill}"; return 1; }
|
|
1904
1906
|
local content; content=$(_skill_content "$skill_file")
|
|
1905
1907
|
case "$agent" in
|
|
1906
|
-
claude) claude -p --output-format
|
|
1908
|
+
claude) claude -p --output-format text "$content" ;;
|
|
1907
1909
|
kimi) kimi --quiet -p "$content" ;;
|
|
1908
1910
|
deepseek) deepseek "$content" ;;
|
|
1909
1911
|
pi) pi -p "$content" ;;
|
|
@@ -2123,6 +2125,9 @@ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
|
2123
2125
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2124
2126
|
_USE_WORKTREE=0
|
|
2125
2127
|
cd "${project_path}" 2>/dev/null || true
|
|
2128
|
+
# US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
|
|
2129
|
+
# post-claude cleanup can diff and delete only this session's additions.
|
|
2130
|
+
CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
|
|
2126
2131
|
if _worktree_fetch_origin main \\
|
|
2127
2132
|
&& _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
|
|
2128
2133
|
_USE_WORKTREE=1
|
|
@@ -2148,6 +2153,14 @@ for _attempt in 1 2 3; do
|
|
|
2148
2153
|
fi
|
|
2149
2154
|
done
|
|
2150
2155
|
|
|
2156
|
+
# US-AUTO-038: diff snapshot vs current and delete any claude/* branches this
|
|
2157
|
+
# session pushed to origin. Runs regardless of claude's exit code (cleanup is
|
|
2158
|
+
# orthogonal to success/failure) and is silent on non-GitHub / unreachable.
|
|
2159
|
+
_claude_cleanup_new_branches "\$CLAUDE_BRANCH_SNAPSHOT" "${project_path}" || true
|
|
2160
|
+
# REFACTOR-011: also prune local .claude/worktrees/ entries whose branch has
|
|
2161
|
+
# been merged to main (remote-branch cleanup above doesn't touch local worktrees).
|
|
2162
|
+
_claude_cleanup_stale_worktrees "${project_path}" || true
|
|
2163
|
+
|
|
2151
2164
|
# Post-claude: publish cycle branch. Doc-only changes (BACKLOG/docs) merge
|
|
2152
2165
|
# immediately via --admin; code changes use auto-merge (CI gate required).
|
|
2153
2166
|
# When \`gh\` is unavailable, fall back to the legacy ff-merge path.
|
|
@@ -2963,15 +2976,73 @@ _loop_precheck_ci() {
|
|
|
2963
2976
|
**Reason**: HEAD CI is red — loop refused to build on a broken base HEAD CI 红,loop 拒绝在破损的基础上构建
|
|
2964
2977
|
|
|
2965
2978
|
**Action required**:
|
|
2966
|
-
- Investigate and fix CI: \`gh -R $
|
|
2979
|
+
- Investigate and fix CI: \`gh -R ${slug} run list --commit ${commit}\`
|
|
2967
2980
|
- After fixing and pushing green commit: \`roll loop now\`
|
|
2968
2981
|
EOF
|
|
2982
|
+
_loop_diagnose_open_prs "$slug" >> "$_LOOP_ALERT"
|
|
2969
2983
|
_notify "roll ⚠ CI red" "loop refused to build on broken base (${short})"
|
|
2970
2984
|
return 1
|
|
2971
2985
|
fi
|
|
2972
2986
|
return 0
|
|
2973
2987
|
}
|
|
2974
2988
|
|
|
2989
|
+
# _loop_diagnose_open_prs <slug>
|
|
2990
|
+
# Appended to ALERT when CI is red on HEAD.
|
|
2991
|
+
# For each open PR targeting main: lists CI failing tests + changed files,
|
|
2992
|
+
# flags whether failures look unrelated to the PR's own changes.
|
|
2993
|
+
_loop_diagnose_open_prs() {
|
|
2994
|
+
local slug="$1"
|
|
2995
|
+
local prs
|
|
2996
|
+
prs=$(gh -R "$slug" pr list --base main --state open --json number,title,headRefName \
|
|
2997
|
+
--jq '.[] | [.number|tostring, .headRefName, .title] | @tsv' 2>/dev/null) || return 0
|
|
2998
|
+
[[ -z "$prs" ]] && return 0
|
|
2999
|
+
|
|
3000
|
+
printf '\n## Open PRs (potential fixes)\n'
|
|
3001
|
+
while IFS=$'\t' read -r pr_num branch pr_title; do
|
|
3002
|
+
printf '\nPR #%s: %s\n' "$pr_num" "$pr_title"
|
|
3003
|
+
|
|
3004
|
+
# Files changed in this PR
|
|
3005
|
+
local changed_files
|
|
3006
|
+
changed_files=$(gh -R "$slug" pr diff "$pr_num" --name-only 2>/dev/null | head -10) || changed_files="(unable to fetch)"
|
|
3007
|
+
printf ' Changed: %s\n' "$(echo "$changed_files" | tr '\n' ' ')"
|
|
3008
|
+
|
|
3009
|
+
# Latest CI run on the PR branch
|
|
3010
|
+
local run_id conclusion
|
|
3011
|
+
run_id=$(gh -R "$slug" run list --branch "$branch" --json databaseId,conclusion \
|
|
3012
|
+
--jq '[.[] | select(.conclusion != null)] | first | .databaseId' 2>/dev/null) || run_id=""
|
|
3013
|
+
conclusion=$(gh -R "$slug" run list --branch "$branch" --json databaseId,conclusion \
|
|
3014
|
+
--jq '[.[] | select(.conclusion != null)] | first | .conclusion' 2>/dev/null) || conclusion="unknown"
|
|
3015
|
+
|
|
3016
|
+
if [[ "$conclusion" == "success" ]]; then
|
|
3017
|
+
printf ' CI: green — blocked only by branch protection (safe to merge)\n'
|
|
3018
|
+
printf ' Suggest: gh pr merge %s --admin\n' "$pr_num"
|
|
3019
|
+
elif [[ -n "$run_id" ]]; then
|
|
3020
|
+
local failing_tests
|
|
3021
|
+
failing_tests=$(gh -R "$slug" run view "$run_id" --log-failed 2>/dev/null \
|
|
3022
|
+
| grep -oP '(?<=not ok \d{1,4} ).*' | head -8) || failing_tests="(unable to fetch)"
|
|
3023
|
+
|
|
3024
|
+
printf ' CI: %s\n' "$conclusion"
|
|
3025
|
+
printf ' Failing tests:\n'
|
|
3026
|
+
while IFS= read -r t; do
|
|
3027
|
+
[[ -n "$t" ]] && printf ' - %s\n' "$t"
|
|
3028
|
+
done <<< "$failing_tests"
|
|
3029
|
+
|
|
3030
|
+
# Heuristic: if no failing test mentions a changed file, flag as likely unrelated
|
|
3031
|
+
local related=0
|
|
3032
|
+
while IFS= read -r f; do
|
|
3033
|
+
local base; base=$(basename "$f")
|
|
3034
|
+
echo "$failing_tests" | grep -qi "$base" && { related=1; break; }
|
|
3035
|
+
done <<< "$changed_files"
|
|
3036
|
+
if [[ "$related" -eq 0 ]]; then
|
|
3037
|
+
printf ' Note: failing tests appear UNRELATED to changed files — consider manual merge\n'
|
|
3038
|
+
printf ' Suggest: gh pr merge %s --admin (verify tests manually first)\n' "$pr_num"
|
|
3039
|
+
else
|
|
3040
|
+
printf ' Note: failing tests may relate to PR changes — investigate before merging\n'
|
|
3041
|
+
fi
|
|
3042
|
+
fi
|
|
3043
|
+
done <<< "$prs"
|
|
3044
|
+
}
|
|
3045
|
+
|
|
2975
3046
|
# CI gate before marking a story Done.
|
|
2976
3047
|
# On CI failure: writes ALERT, returns 1 (caller keeps story 🔨 In Progress).
|
|
2977
3048
|
# When gh unavailable: returns 0 (graceful skip).
|
|
@@ -3101,6 +3172,219 @@ _loop_is_manual_only() {
|
|
|
3101
3172
|
echo "$row" | grep -qE 'manual-only:true'
|
|
3102
3173
|
}
|
|
3103
3174
|
|
|
3175
|
+
# US-AUTO-034: PR-first inbox — loop processes open PRs before scanning BACKLOG.
|
|
3176
|
+
#
|
|
3177
|
+
# Three building blocks, kept as pure / mockable functions:
|
|
3178
|
+
# _loop_pr_classify pure routing decision (no side effects)
|
|
3179
|
+
# _loop_pr_rebase_circuit 24h sliding-window circuit breaker on rebase retries
|
|
3180
|
+
# _loop_pr_inbox orchestrator that walks `gh pr list` and routes
|
|
3181
|
+
# each open PR to skip / review / rebase
|
|
3182
|
+
#
|
|
3183
|
+
# Design notes:
|
|
3184
|
+
# - gh missing or any gh failure → return 0 (lenient, like FIX-026's pre-check)
|
|
3185
|
+
# - self-authored loop/* PRs are skipped to avoid same-source AI review
|
|
3186
|
+
# - latest human review of CHANGES_REQUESTED or APPROVED blocks AI review
|
|
3187
|
+
# (Human-review-activity guard from US-AUTO-034 AC)
|
|
3188
|
+
# - rebase attempts ≥3 within 24h trip the circuit breaker (writes ALERT)
|
|
3189
|
+
|
|
3190
|
+
# _loop_pr_classify <head_ref> <human_review_state> <ci_state> <mergeable_state>
|
|
3191
|
+
# Prints one of:
|
|
3192
|
+
# loop_self
|
|
3193
|
+
# blocked_human_request_changes
|
|
3194
|
+
# blocked_human_approved
|
|
3195
|
+
# stale
|
|
3196
|
+
# eligible
|
|
3197
|
+
# Exit 0 always — callers parse the printed token.
|
|
3198
|
+
_loop_pr_classify() {
|
|
3199
|
+
local head_ref="${1:-}"
|
|
3200
|
+
local human_review="${2:-}"
|
|
3201
|
+
local ci_state="${3:-}"
|
|
3202
|
+
local mergeable="${4:-}"
|
|
3203
|
+
|
|
3204
|
+
case "$head_ref" in
|
|
3205
|
+
loop/*) echo "loop_self"; return 0 ;;
|
|
3206
|
+
esac
|
|
3207
|
+
|
|
3208
|
+
case "$human_review" in
|
|
3209
|
+
CHANGES_REQUESTED) echo "blocked_human_request_changes"; return 0 ;;
|
|
3210
|
+
APPROVED) echo "blocked_human_approved"; return 0 ;;
|
|
3211
|
+
esac
|
|
3212
|
+
|
|
3213
|
+
if [ "$ci_state" = "failure" ] || [ "$mergeable" = "CONFLICTING" ] || [ "$mergeable" = "BEHIND" ]; then
|
|
3214
|
+
echo "stale"
|
|
3215
|
+
return 0
|
|
3216
|
+
fi
|
|
3217
|
+
|
|
3218
|
+
echo "eligible"
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
# _loop_pr_rebase_circuit <pr_number>
|
|
3222
|
+
# Side effect: appends current timestamp to $_LOOP_STATE under
|
|
3223
|
+
# pr_state.<PR>.attempts_at, pruning entries older than 24h.
|
|
3224
|
+
# Exit 0: attempt allowed and recorded.
|
|
3225
|
+
# Exit 1: ≥3 attempts within 24h → blocked; ALERT written.
|
|
3226
|
+
_loop_pr_rebase_circuit() {
|
|
3227
|
+
local pr="$1"
|
|
3228
|
+
[ -n "$pr" ] || return 1
|
|
3229
|
+
|
|
3230
|
+
local state="${_LOOP_STATE:-${HOME}/.shared/roll/loop/state.yaml}"
|
|
3231
|
+
local now; now=$(date -u +%s)
|
|
3232
|
+
local cutoff=$((now - 86400))
|
|
3233
|
+
|
|
3234
|
+
# Extract existing timestamps for this PR (empty if absent).
|
|
3235
|
+
local existing=""
|
|
3236
|
+
if [ -f "$state" ]; then
|
|
3237
|
+
existing=$(awk -v pr="\"$pr\":" '
|
|
3238
|
+
$0 ~ "pr_state:" {in_pr=1; next}
|
|
3239
|
+
in_pr && $0 ~ pr {in_target=1; next}
|
|
3240
|
+
in_target && $0 ~ /attempts_at:/ {
|
|
3241
|
+
sub(/^[^"]*"/, ""); sub(/".*$/, ""); print; exit
|
|
3242
|
+
}
|
|
3243
|
+
in_target && /^[^[:space:]]/ {in_target=0}
|
|
3244
|
+
' "$state" 2>/dev/null)
|
|
3245
|
+
fi
|
|
3246
|
+
|
|
3247
|
+
# Prune stale timestamps (>24h ago).
|
|
3248
|
+
local fresh=""
|
|
3249
|
+
local ts
|
|
3250
|
+
for ts in $existing; do
|
|
3251
|
+
case "$ts" in
|
|
3252
|
+
''|*[!0-9]*) continue ;;
|
|
3253
|
+
esac
|
|
3254
|
+
if [ "$ts" -ge "$cutoff" ]; then
|
|
3255
|
+
fresh="${fresh:+$fresh }$ts"
|
|
3256
|
+
fi
|
|
3257
|
+
done
|
|
3258
|
+
|
|
3259
|
+
# Count attempts within window; ≥3 means this would be the 4th retry blocked.
|
|
3260
|
+
local count=0
|
|
3261
|
+
for ts in $fresh; do count=$((count + 1)); done
|
|
3262
|
+
|
|
3263
|
+
if [ "$count" -ge 3 ]; then
|
|
3264
|
+
mkdir -p "$(dirname "${_LOOP_ALERT:-/dev/null}")" 2>/dev/null || true
|
|
3265
|
+
cat > "${_LOOP_ALERT}" <<EOF
|
|
3266
|
+
# ALERT — PR rebase circuit breaker tripped
|
|
3267
|
+
|
|
3268
|
+
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
3269
|
+
**PR**: #${pr}
|
|
3270
|
+
**Reason**: PR #${pr} rebased ${count}× within 24h without CI resolution — possible workflow file error PR rebase 多次未解决,可能是 workflow 文件错误
|
|
3271
|
+
|
|
3272
|
+
**Action required**:
|
|
3273
|
+
- Check PR CI logs and workflow files for breakage
|
|
3274
|
+
- Resolve manually, then: \`roll loop now\`
|
|
3275
|
+
EOF
|
|
3276
|
+
return 1
|
|
3277
|
+
fi
|
|
3278
|
+
|
|
3279
|
+
# Record this attempt and persist.
|
|
3280
|
+
fresh="${fresh:+$fresh }$now"
|
|
3281
|
+
_loop_pr_state_write "$pr" "$fresh" "$state"
|
|
3282
|
+
return 0
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
# Internal: rewrite $state with pr_state.<pr>.attempts_at = "<fresh-ts-list>".
|
|
3286
|
+
# Minimal YAML writer — we own the schema and only need this one field family.
|
|
3287
|
+
_loop_pr_state_write() {
|
|
3288
|
+
local pr="$1"
|
|
3289
|
+
local attempts="$2"
|
|
3290
|
+
local state="$3"
|
|
3291
|
+
|
|
3292
|
+
mkdir -p "$(dirname "$state")" 2>/dev/null || true
|
|
3293
|
+
[ -f "$state" ] || : > "$state"
|
|
3294
|
+
|
|
3295
|
+
local tmp; tmp=$(mktemp)
|
|
3296
|
+
awk -v pr="\"$pr\":" -v attempts="$attempts" '
|
|
3297
|
+
BEGIN { in_pr=0; in_target=0; written=0 }
|
|
3298
|
+
/^pr_state:/ { in_pr=1; print; next }
|
|
3299
|
+
in_pr && $0 ~ pr {
|
|
3300
|
+
in_target=1
|
|
3301
|
+
print " " pr
|
|
3302
|
+
print " attempts_at: \"" attempts "\""
|
|
3303
|
+
written=1
|
|
3304
|
+
next
|
|
3305
|
+
}
|
|
3306
|
+
in_target && /attempts_at:/ { next } # skip old value, already written
|
|
3307
|
+
in_target && /^[^[:space:]]/ { in_target=0 }
|
|
3308
|
+
{ print }
|
|
3309
|
+
END {
|
|
3310
|
+
if (!in_pr) {
|
|
3311
|
+
print "pr_state:"
|
|
3312
|
+
print " " pr
|
|
3313
|
+
print " attempts_at: \"" attempts "\""
|
|
3314
|
+
} else if (!written) {
|
|
3315
|
+
print " " pr
|
|
3316
|
+
print " attempts_at: \"" attempts "\""
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
# _loop_pr_inbox
|
|
3323
|
+
# Walks open PRs and routes each by classification.
|
|
3324
|
+
# Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
|
|
3325
|
+
_loop_pr_inbox() {
|
|
3326
|
+
command -v gh >/dev/null 2>&1 || return 0
|
|
3327
|
+
|
|
3328
|
+
local slug; slug=$(_gh_repo_slug 2>/dev/null) || return 0
|
|
3329
|
+
local prs_json
|
|
3330
|
+
prs_json=$(gh -R "$slug" pr list --state open \
|
|
3331
|
+
--json number,headRefName,author,title \
|
|
3332
|
+
2>/dev/null) || return 0
|
|
3333
|
+
[ -n "$prs_json" ] || return 0
|
|
3334
|
+
[ "$prs_json" = "[]" ] && return 0
|
|
3335
|
+
|
|
3336
|
+
local count; count=$(echo "$prs_json" | jq 'length' 2>/dev/null || echo 0)
|
|
3337
|
+
[ "${count:-0}" -gt 0 ] || return 0
|
|
3338
|
+
|
|
3339
|
+
local i=0
|
|
3340
|
+
while [ "$i" -lt "$count" ]; do
|
|
3341
|
+
local num head_ref
|
|
3342
|
+
num=$(echo "$prs_json" | jq -r ".[$i].number")
|
|
3343
|
+
head_ref=$(echo "$prs_json" | jq -r ".[$i].headRefName")
|
|
3344
|
+
|
|
3345
|
+
# Fetch CI + review state for this PR.
|
|
3346
|
+
local view_json
|
|
3347
|
+
view_json=$(gh -R "$slug" pr view "$num" \
|
|
3348
|
+
--json reviews,mergeStateStatus,statusCheckRollup \
|
|
3349
|
+
2>/dev/null) || { i=$((i + 1)); continue; }
|
|
3350
|
+
|
|
3351
|
+
local human_review ci_state mergeable
|
|
3352
|
+
human_review=$(echo "$view_json" | jq -r '
|
|
3353
|
+
[.reviews[]? | select(.authorAssociation != "BOT" and .authorAssociation != "APP")]
|
|
3354
|
+
| last // {} | .state // ""' 2>/dev/null)
|
|
3355
|
+
mergeable=$(echo "$view_json" | jq -r '.mergeStateStatus // ""' 2>/dev/null)
|
|
3356
|
+
ci_state=$(echo "$view_json" | jq -r '
|
|
3357
|
+
if (.statusCheckRollup | length) == 0 then ""
|
|
3358
|
+
elif any(.statusCheckRollup[]?; .conclusion == "FAILURE") then "failure"
|
|
3359
|
+
elif all(.statusCheckRollup[]?; .conclusion == "SUCCESS" or .conclusion == "SKIPPED") then "success"
|
|
3360
|
+
else "pending" end' 2>/dev/null)
|
|
3361
|
+
|
|
3362
|
+
local verdict
|
|
3363
|
+
verdict=$(_loop_pr_classify "$head_ref" "$human_review" "$ci_state" "$mergeable")
|
|
3364
|
+
|
|
3365
|
+
case "$verdict" in
|
|
3366
|
+
loop_self|blocked_human_request_changes|blocked_human_approved)
|
|
3367
|
+
: # skip — explained by verdict; nothing to do this cycle
|
|
3368
|
+
;;
|
|
3369
|
+
stale)
|
|
3370
|
+
_loop_pr_rebase_circuit "$num" || true
|
|
3371
|
+
# Actual rebase work delegated to _loop_pr_rebase_stale when present
|
|
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
|
|
3375
|
+
;;
|
|
3376
|
+
eligible)
|
|
3377
|
+
# Hand off to review (US-AUTO-035 supplies the actual decision).
|
|
3378
|
+
command -v _loop_pr_review_external >/dev/null 2>&1 \
|
|
3379
|
+
&& _loop_pr_review_external "$num" || true
|
|
3380
|
+
;;
|
|
3381
|
+
esac
|
|
3382
|
+
|
|
3383
|
+
i=$((i + 1))
|
|
3384
|
+
done
|
|
3385
|
+
return 0
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3104
3388
|
# US-CL-004: changelog 风格守门 Phase 1 — mechanical linter.
|
|
3105
3389
|
#
|
|
3106
3390
|
# _changelog_lint_bullet <bullet-text>
|
|
@@ -3156,7 +3440,7 @@ _changelog_style_anchors() {
|
|
|
3156
3440
|
/^## v/ { ver++; if (ver > 3) exit; printing = 1; next }
|
|
3157
3441
|
/^## / { printing = 0 }
|
|
3158
3442
|
printing && /^- / { print }
|
|
3159
|
-
' "$changelog" | head -c 1500
|
|
3443
|
+
' "$changelog" | head -c 1500 || true
|
|
3160
3444
|
}
|
|
3161
3445
|
|
|
3162
3446
|
# US-CL-005: changelog 风格守门 Phase 2 — self-audit gate.
|
|
@@ -3362,6 +3646,75 @@ _worktree_merge_back() {
|
|
|
3362
3646
|
return 0
|
|
3363
3647
|
}
|
|
3364
3648
|
|
|
3649
|
+
# _claude_remote_snapshot [repo]
|
|
3650
|
+
# Echo the current set of remote `claude/*` branch names (sans
|
|
3651
|
+
# refs/heads/), one per line, sorted. Silent on remote unreachable / no
|
|
3652
|
+
# remote / no matches — empty stdout, exit 0.
|
|
3653
|
+
_claude_remote_snapshot() {
|
|
3654
|
+
local repo="${1:-.}"
|
|
3655
|
+
git -C "$repo" ls-remote --heads origin 'refs/heads/claude/*' 2>/dev/null \
|
|
3656
|
+
| awk '{print $2}' \
|
|
3657
|
+
| sed 's|^refs/heads/||' \
|
|
3658
|
+
| sort
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
# _claude_cleanup_new_branches <prior> [repo]
|
|
3662
|
+
# Delete remote `claude/*` branches present now but absent from <prior>
|
|
3663
|
+
# (newline-separated list, as emitted by _claude_remote_snapshot). Skips
|
|
3664
|
+
# silently when origin is not a GitHub remote. Each successful delete logs
|
|
3665
|
+
# one INFO line; failures are silently ignored so the loop's main flow is
|
|
3666
|
+
# never derailed.
|
|
3667
|
+
_claude_cleanup_new_branches() {
|
|
3668
|
+
local prior="$1"
|
|
3669
|
+
local repo="${2:-.}"
|
|
3670
|
+
local url; url=$(git -C "$repo" remote get-url origin 2>/dev/null)
|
|
3671
|
+
[[ "$url" == *github.com* ]] || return 0
|
|
3672
|
+
local current; current=$(_claude_remote_snapshot "$repo")
|
|
3673
|
+
[ -z "$current" ] && return 0
|
|
3674
|
+
local prior_sorted; prior_sorted=$(printf '%s\n' "$prior" | sort -u)
|
|
3675
|
+
local new_branches
|
|
3676
|
+
new_branches=$(comm -13 <(printf '%s\n' "$prior_sorted") <(printf '%s\n' "$current"))
|
|
3677
|
+
[ -z "$new_branches" ] && return 0
|
|
3678
|
+
while IFS= read -r branch; do
|
|
3679
|
+
[ -z "$branch" ] && continue
|
|
3680
|
+
if git -C "$repo" push origin --delete "$branch" 2>/dev/null; then
|
|
3681
|
+
echo "[loop] deleted stale claude branch: $branch"
|
|
3682
|
+
fi
|
|
3683
|
+
done <<< "$new_branches"
|
|
3684
|
+
return 0
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
# _claude_cleanup_stale_worktrees [project_path]
|
|
3688
|
+
# Remove local worktrees under <project_path>/.claude/worktrees/ whose
|
|
3689
|
+
# branch has been fully merged into main (merge-base --is-ancestor). Active
|
|
3690
|
+
# worktrees (branch ahead of main) are preserved. Runs `git worktree prune`
|
|
3691
|
+
# afterwards to clear stale metadata. Silent on missing directory or any
|
|
3692
|
+
# individual failure so the loop's main flow is never derailed.
|
|
3693
|
+
_claude_cleanup_stale_worktrees() {
|
|
3694
|
+
local project_path="${1:-.}"
|
|
3695
|
+
local wt_dir="${project_path}/.claude/worktrees"
|
|
3696
|
+
[ -d "$wt_dir" ] || return 0
|
|
3697
|
+
local entry branch
|
|
3698
|
+
for entry in "$wt_dir"/*/; do
|
|
3699
|
+
[ -d "$entry" ] || continue
|
|
3700
|
+
branch=$(git -C "$project_path" worktree list --porcelain 2>/dev/null \
|
|
3701
|
+
| awk -v p="${entry%/}" '
|
|
3702
|
+
/^worktree / { cur=$2; flag=(cur==p) }
|
|
3703
|
+
/^branch / && flag { sub(/^refs\/heads\//, "", $2); print $2; flag=0 }
|
|
3704
|
+
')
|
|
3705
|
+
[ -z "$branch" ] && branch=$(git -C "$entry" symbolic-ref --short HEAD 2>/dev/null)
|
|
3706
|
+
[ -z "$branch" ] && continue
|
|
3707
|
+
if git -C "$project_path" merge-base --is-ancestor "$branch" main 2>/dev/null; then
|
|
3708
|
+
git -C "$project_path" worktree remove --force "$entry" 2>/dev/null || true
|
|
3709
|
+
rm -rf "$entry" 2>/dev/null || true
|
|
3710
|
+
git -C "$project_path" branch -D "$branch" 2>/dev/null || true
|
|
3711
|
+
echo "[loop] removed stale worktree: $branch"
|
|
3712
|
+
fi
|
|
3713
|
+
done
|
|
3714
|
+
git -C "$project_path" worktree prune 2>/dev/null || true
|
|
3715
|
+
return 0
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3365
3718
|
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
3366
3719
|
#
|
|
3367
3720
|
# _loop_publish_pr <branch> [title]
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
- Requests made in conversation are NOT AC — capture with `roll-idea` first.
|
|
35
35
|
- Any new Story/Fix requires design doc + user confirmation before TCR starts.
|
|
36
36
|
- Do not commit without user approval unless explicitly told to auto-commit.
|
|
37
|
+
- **Goal First**: Before any implementation, state verifiable success criteria.
|
|
38
|
+
Transform vague tasks: "add validation" → "write test for invalid input, then make it pass".
|
|
39
|
+
Multi-step work: list steps with verify checkpoints (step → verify: how to check).
|
|
40
|
+
Weak criteria ("make it work") require human clarification before starting.
|
|
37
41
|
- **TCR**: Test -> Green = Commit / Red = Revert. No WIP commits.
|
|
38
42
|
- Before implementing: confirm exact files, test strategy, and commit message
|
|
39
43
|
draft with user. Do not write code until approved.
|
|
@@ -83,3 +87,9 @@ Confirm each phase clean before proceeding to the next.
|
|
|
83
87
|
- `components/ui/` is shadcn-generated — never edit manually.
|
|
84
88
|
- Tailwind utility classes only. No inline styles, no CSS modules.
|
|
85
89
|
- Icons: Lucide React.
|
|
90
|
+
|
|
91
|
+
## 8. Where to Look
|
|
92
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
93
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
94
|
+
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
|
95
|
+
- When `docs/domain/` or `docs/features/` don't exist yet, run `$roll-doc` to bootstrap.
|
|
@@ -25,3 +25,8 @@ src/
|
|
|
25
25
|
- **TCR**: Mandatory.
|
|
26
26
|
- **Security**: Input validation (zod), Rate limiting, Secrets rotation.
|
|
27
27
|
- **Workspace**: `BACKLOG.md` + `docs/features/`.
|
|
28
|
+
|
|
29
|
+
## 5. Where to Look
|
|
30
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
31
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
32
|
+
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
|
@@ -27,3 +27,8 @@ tests/
|
|
|
27
27
|
- **TCR**: Mandatory.
|
|
28
28
|
- **Distribution**: `bin` in `package.json`, test `npm i -g`.
|
|
29
29
|
- **Workspace**: `BACKLOG.md` + `docs/features/`.
|
|
30
|
+
|
|
31
|
+
## 5. Where to Look
|
|
32
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
33
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
34
|
+
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
|
@@ -24,3 +24,8 @@ src/
|
|
|
24
24
|
- **TCR**: Mandatory.
|
|
25
25
|
- **Testing**: Unit (hooks/logic) >80%, E2E (Playwright).
|
|
26
26
|
- **Workspace**: `BACKLOG.md` + `docs/features/`.
|
|
27
|
+
|
|
28
|
+
## 5. Where to Look
|
|
29
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
30
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
31
|
+
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
|
@@ -27,3 +27,8 @@ api/
|
|
|
27
27
|
- **TCR**: Mandatory.
|
|
28
28
|
- **Testing**: Unit >80%, E2E for critical flows.
|
|
29
29
|
- **Workspace**: `BACKLOG.md` + `docs/features/`.
|
|
30
|
+
|
|
31
|
+
## 5. Where to Look
|
|
32
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
33
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
34
|
+
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
package/package.json
CHANGED
|
@@ -491,6 +491,19 @@ OrderPricingService:
|
|
|
491
491
|
Domain: Order Context > Order Aggregate > OrderItem Entity
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
+
### AGENTS.md Where to Look — 指针维护
|
|
495
|
+
|
|
496
|
+
After completing any Domain Slice (User Story level), check if the project's `AGENTS.md` has a `## Where to Look` section with a `docs/domain/` pointer. If missing, append one line:
|
|
497
|
+
|
|
498
|
+
```markdown
|
|
499
|
+
- **Domain model**: `docs/domain/context-map.md` — Bounded Contexts and relationships
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Rules:
|
|
503
|
+
- Idempotent: only append if the pointer line is not already present
|
|
504
|
+
- Do not modify any other content in AGENTS.md
|
|
505
|
+
- Skip silently if `docs/domain/` does not yet exist for this project
|
|
506
|
+
|
|
494
507
|
---
|
|
495
508
|
|
|
496
509
|
## Clarify Phase
|
|
@@ -505,10 +518,20 @@ Domain: Order Context > Order Aggregate > OrderItem Entity
|
|
|
505
518
|
- Contains ambiguous terms like "优化一下", "改一下", "加个东西", "做个设计"
|
|
506
519
|
- Could be interpreted in multiple ways
|
|
507
520
|
|
|
521
|
+
**Pre-Clarify: three-step product localization (always run first, silently)**
|
|
522
|
+
|
|
523
|
+
Before listing questions, internally determine:
|
|
524
|
+
1. **产品端 (product end)**: web / mobile / API / CLI / other — which surface does this touch?
|
|
525
|
+
2. **角色 (role)**: who initiates this action? (end user / admin / system / external)
|
|
526
|
+
3. **业务域 (domain)**: which business domain does this belong to?
|
|
527
|
+
|
|
528
|
+
Already-localized dimensions become context prefix in the output, not open questions.
|
|
529
|
+
|
|
508
530
|
**Output format:**
|
|
509
531
|
|
|
510
532
|
```
|
|
511
533
|
🎯 Clarified Intent: {1-2 sentences}
|
|
534
|
+
🗺 Context: {product end} · {role} · {domain} ← omit if all three are unknown
|
|
512
535
|
|
|
513
536
|
📏 Complexity: {small|medium|large}
|
|
514
537
|
|
package/skills/roll-doc/SKILL.md
CHANGED
|
@@ -124,6 +124,29 @@ For each gap:
|
|
|
124
124
|
| Module with no README | `<dir>/README.md` |
|
|
125
125
|
| No `docs/domain/` entries | `docs/domain/context-map.md` |
|
|
126
126
|
| No conventions doc | `docs/CONVENTIONS.md` |
|
|
127
|
+
| Missing `AGENTS.md` `## Where to Look` section | `AGENTS.md` (append or create) |
|
|
128
|
+
|
|
129
|
+
**AGENTS.md Where to Look bootstrap:**
|
|
130
|
+
|
|
131
|
+
When `AGENTS.md` has no `## Where to Look` section, generate and append one:
|
|
132
|
+
|
|
133
|
+
1. Scan which doc directories actually exist: `docs/domain/`, `docs/features/`, `docs/practices/`, etc.
|
|
134
|
+
2. Generate pointer lines **only for directories that actually exist** — never fabricate pointers to missing paths
|
|
135
|
+
3. If `docs/domain/context-map.md` exists, read it to extract Bounded Context names for a one-line summary
|
|
136
|
+
4. Append the section to the end of `AGENTS.md` with the standard draft header
|
|
137
|
+
5. **Idempotency**: if `## Where to Look` already present, do not overwrite or duplicate — skip this gap
|
|
138
|
+
|
|
139
|
+
Draft output format:
|
|
140
|
+
|
|
141
|
+
```markdown
|
|
142
|
+
> **Draft** — auto-generated by roll-doc on YYYY-MM-DD. Review before treating as authoritative.
|
|
143
|
+
|
|
144
|
+
## Where to Look
|
|
145
|
+
- **Domain model**: `docs/domain/context-map.md` — Contexts: {list from context-map, or "see file"}
|
|
146
|
+
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Only include lines for directories that already exist in the project.
|
|
127
150
|
|
|
128
151
|
**Every generated file starts with this exact header line:**
|
|
129
152
|
|
|
@@ -84,6 +84,33 @@ exact stuck-red state FIX-026 traces to).
|
|
|
84
84
|
- `gh` missing or repo unparseable → graceful skip (`_loop_precheck_ci`
|
|
85
85
|
returns 0); the post-build `_loop_enforce_ci` remains the strict gate.
|
|
86
86
|
|
|
87
|
+
### Step 1.6 — PR Inbox (US-AUTO-034)
|
|
88
|
+
|
|
89
|
+
Before scanning BACKLOG, process open PRs first. PRs are also units of work:
|
|
90
|
+
external contributors and human teammates expect their PRs to be reviewed and
|
|
91
|
+
moved forward, not starved while loop opens new fronts.
|
|
92
|
+
|
|
93
|
+
Call `_loop_pr_inbox` after the pre-run CI check passes. It walks
|
|
94
|
+
`gh pr list --state open` and routes each PR by classification:
|
|
95
|
+
|
|
96
|
+
| Classification | Action |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `loop_self` (head ref starts with `loop/`) | Skip — let GitHub auto-merge handle it; never AI-review your own commit |
|
|
99
|
+
| `blocked_human_request_changes` | Skip — last human review requested changes; wait for the author to push fixes |
|
|
100
|
+
| `blocked_human_approved` | Skip — let GitHub auto-merge after CI is green |
|
|
101
|
+
| `stale` (CI failed or branch behind/conflicting) | Try `_loop_pr_rebase_stale` after the circuit breaker allows it |
|
|
102
|
+
| `eligible` (clean external PR, no blocking review) | Invoke `_loop_pr_review_external` — the actual decision is provided by US-AUTO-035's GitHub Action |
|
|
103
|
+
|
|
104
|
+
**Rebase circuit breaker** — `_loop_pr_rebase_circuit <pr>` records each rebase
|
|
105
|
+
attempt under `pr_state.<PR>.attempts_at` in `state.yaml`, pruning entries older
|
|
106
|
+
than 24 h. Once ≥3 attempts land within 24 h, further rebases are blocked and an
|
|
107
|
+
ALERT is written (typical cause: a broken workflow file makes CI never run,
|
|
108
|
+
which would otherwise drive infinite rebase loops).
|
|
109
|
+
|
|
110
|
+
**Lenient on infrastructure** — `gh` missing, repo unparseable, or any
|
|
111
|
+
`gh` API failure → `_loop_pr_inbox` returns 0 and the loop falls through to
|
|
112
|
+
Step 2 (BACKLOG scan). Same posture as the pre-run CI check.
|
|
113
|
+
|
|
87
114
|
### Step 2 — Scan BACKLOG
|
|
88
115
|
|
|
89
116
|
Read `BACKLOG.md`. Collect all rows where Status = `📋 Todo`, in order:
|