@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 CHANGED
@@ -1,12 +1,26 @@
1
1
  # Changelog
2
2
 
3
- ## v2026.514.2
3
+ ## v2026.514.5
4
4
 
5
- - Changelog 历史版本全部重新整理:按主题分组、合并同类项、用词更贴近用户视角,附 `[loop]` / `[dream]` 归因标记
6
- - Changelog Skill 新增 Release Notes 生成规范:分组规则、条目合并、归因标签、措辞原则
7
- - README 新增 Evolution 章节,梳理 Roll 从工具到自主交付系统的演进脉络
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
- ## v2026.514.1
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
- > Roll out features with AI agents — _Move fast, no sprints._
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.2"
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 "$skill_dir" "$skill_link"
468
+ # macOS ln -sf follows symlinks-to-dirs and creates inside instead of
469
+ # replacing — explicitly remove first to guarantee replacement.
470
+ rm -f "$skill_link" && ln -s "$skill_dir" "$skill_link"
469
471
  repaired=$((repaired + 1))
470
472
  fi
471
473
  # correct symlink: skip silently
@@ -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 stream-json "$content" ;;
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 $(_gh_repo_slug) run list --commit ${commit}\`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.514.2",
3
+ "version": "2026.514.5",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -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
 
@@ -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: