@seanyao/roll 2026.516.1 → 2026.517.2

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,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.517.2
4
+ - **Fixed**: `roll dream`、`roll brief`、`roll loop` 的定时任务不再被 Claude 升级后的弹窗拦住,悄悄失效
5
+
6
+ ## v2026.517.1
7
+
8
+ - **New**: loop 自动修复 story 引入的 CI 红 — 不再每次 CI 红都停下等人,修不好才写 ALERT `[loop]`
9
+ - **New**: Roll 官网上线 — 装、用、原理一站讲清楚
10
+ - **Fixed**: mac 休眠不再打断 loop cycle — 全程保持唤醒 `[loop]`
11
+ - **Fixed**: agent 假死时 loop 自动接管,不再无限挂起 `[loop]`
12
+ - **Fixed**: PR / 合并失败时 — loop 仍能把代码备份到独立分支不丢失 `[loop]`
13
+ - **Fixed**: loop 启动时自动恢复上一轮中断的工作,意外中断的代码不再失踪 `[loop]`
14
+ - **Fixed**: `roll loop now` 现在卡住状态也会先自愈再启动 `[loop]`
15
+ - **Fixed**: 自治 loop 不再被权限弹窗卡住 `[loop]`
16
+ - **Fixed**: `roll peer` 多轮 review 不再中途断线 `[peer]`
17
+ - **Fixed**: `roll loop runs` 现在跨子目录都能显示历史 `[loop]`
18
+ - **Fixed**: loop 空跑也会清理 worktree,不再随时间堆积 `[loop]`
19
+
3
20
  ## v2026.515.1
4
21
 
5
22
  - **New**: `roll brief` / `roll dream` 生成文档后自动提交推送 — 每次晨报和夜检不再需要手动 commit `[loop]`
@@ -170,7 +187,6 @@ Roll 从「技能编排工具」变成了「自主执行系统」——三个新
170
187
 
171
188
  ## 2026.05.06
172
189
  - **Added**: OpenCode 集成 — 检测 opencode 环境,自动同步全局 AGENTS.md 规则文件
173
- - **Added**: roll-bipo-onboard 技能 — 新员工入职引导流程技能,含 bats 测试
174
190
  - **Improved**: Git 提交归属 — 用 Co-Authored-By trailer 替代 [client] 前缀,更标准的多 AI 工具归属方式
175
191
  - **Improved**: AGENTS.md 加入 Scope Gate — 防止技能执行时越界修改不相关文件
176
192
 
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.516.1"
7
+ VERSION="2026.517.2"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -1493,45 +1493,19 @@ _peer_call() {
1493
1493
  local out_file
1494
1494
  out_file=$(mktemp)
1495
1495
  local cmd_str
1496
- case "$to" in
1497
- claude) cmd_str="claude -p --output-format text $(printf %q "$prompt")" ;;
1498
- kimi) cmd_str="kimi --quiet -p $(printf %q "$prompt")" ;;
1499
- pi) cmd_str="pi -p $(printf %q "$prompt")" ;;
1500
- deepseek) cmd_str="deepseek $(printf %q "$prompt")" ;;
1501
- codex) cmd_str="codex exec --json --output-last-message $(printf %q "$prompt")" ;;
1502
- opencode) cmd_str="opencode run $(printf %q "$prompt")" ;;
1503
- *)
1504
- err "Unsupported peer: $to 不支持的 peer: $to"
1505
- return 1 ;;
1506
- esac
1496
+ cmd_str=$(_agent_cmd_str "$to" peer "$prompt") || {
1497
+ err "Unsupported peer: $to 不支持的 peer: $to"
1498
+ return 1
1499
+ }
1507
1500
  _peer_dispatch_in_tmux "$session" "$cmd_str" "$out_file" "$stderr_log" "$call_timeout"
1508
1501
  output="$(cat "$out_file" 2>/dev/null || true)"
1509
1502
  rm -f "$out_file"
1510
1503
  else
1511
- case "$to" in
1512
- claude)
1513
- output="$(claude -p --output-format text "$prompt" 2>"$stderr_log" || true)"
1514
- ;;
1515
- kimi)
1516
- output="$(kimi --quiet -p "$prompt" 2>"$stderr_log" || true)"
1517
- ;;
1518
- pi)
1519
- output="$(pi -p "$prompt" 2>"$stderr_log" || true)"
1520
- ;;
1521
- deepseek)
1522
- output="$(deepseek "$prompt" 2>"$stderr_log" || true)"
1523
- ;;
1524
- codex)
1525
- output="$(codex exec --json --output-last-message "$prompt" 2>"$stderr_log" || true)"
1526
- ;;
1527
- opencode)
1528
- output="$(opencode run "$prompt" 2>"$stderr_log" || true)"
1529
- ;;
1530
- *)
1531
- err "Unsupported peer: $to 不支持的 peer: $to"
1532
- return 1
1533
- ;;
1534
- esac
1504
+ _agent_argv "$to" peer "$prompt" || {
1505
+ err "Unsupported peer: $to 不支持的 peer: $to"
1506
+ return 1
1507
+ }
1508
+ output="$("${_AGENT_ARGV[@]}" 2>"$stderr_log" || true)"
1535
1509
  fi
1536
1510
 
1537
1511
  printf '%s\n' "$output"
@@ -1884,21 +1858,71 @@ _parse_review_verdict() {
1884
1858
  echo "${type}${reason:+:${reason}}"
1885
1859
  }
1886
1860
 
1861
+ # REFACTOR-017: single source of truth for agent invocation argv.
1862
+ # Sets the global _AGENT_ARGV array to the command + args for (agent, mode, prompt).
1863
+ # Modes:
1864
+ # text — structured text (claude --output-format text; codex exec)
1865
+ # plain — default output (claude -p; codex exec)
1866
+ # peer — peer protocol (claude --output-format text; codex --json --output-last-message)
1867
+ # Returns 1 on unknown agent. Adding a new agent only needs an entry here.
1868
+ _agent_argv() {
1869
+ local agent="$1" mode="$2" prompt="$3"
1870
+ case "$agent" in
1871
+ claude)
1872
+ case "$mode" in
1873
+ text|peer) _AGENT_ARGV=(claude -p --output-format text "$prompt") ;;
1874
+ *) _AGENT_ARGV=(claude -p "$prompt") ;;
1875
+ esac ;;
1876
+ kimi) _AGENT_ARGV=(kimi --quiet -p "$prompt") ;;
1877
+ deepseek) _AGENT_ARGV=(deepseek "$prompt") ;;
1878
+ pi) _AGENT_ARGV=(pi -p "$prompt") ;;
1879
+ codex)
1880
+ case "$mode" in
1881
+ peer) _AGENT_ARGV=(codex exec --json --output-last-message "$prompt") ;;
1882
+ *) _AGENT_ARGV=(codex exec "$prompt") ;;
1883
+ esac ;;
1884
+ opencode) _AGENT_ARGV=(opencode run "$prompt") ;;
1885
+ *) return 1 ;;
1886
+ esac
1887
+ }
1888
+
1889
+ # Build a printf %q-escaped command string for (agent, mode, prompt).
1890
+ # Used where the command must be passed as a string (e.g. tmux send-keys).
1891
+ _agent_cmd_str() {
1892
+ _agent_argv "$@" || return 1
1893
+ local i out
1894
+ printf -v out '%q' "${_AGENT_ARGV[0]}"
1895
+ for ((i = 1; i < ${#_AGENT_ARGV[@]}; i++)); do
1896
+ printf -v out '%s %q' "$out" "${_AGENT_ARGV[i]}"
1897
+ done
1898
+ printf '%s' "$out"
1899
+ }
1900
+
1901
+ # Splice --dangerously-skip-permissions into _AGENT_ARGV for claude. Used by
1902
+ # trusted, human-triggered, or autonomous flows that should not be blocked by
1903
+ # Claude Code's pre-write "approve diff" UX (which silently never gets
1904
+ # approved in `claude -p` pipe mode). No-op for non-claude agents and for
1905
+ # already-bypassed argvs.
1906
+ _agent_bypass_claude_perms() {
1907
+ [[ "${_AGENT_ARGV[0]}" == "claude" ]] || return 0
1908
+ local arg
1909
+ for arg in "${_AGENT_ARGV[@]}"; do
1910
+ [[ "$arg" == "--dangerously-skip-permissions" ]] && return 0
1911
+ done
1912
+ _AGENT_ARGV=("${_AGENT_ARGV[@]:0:2}" --dangerously-skip-permissions "${_AGENT_ARGV[@]:2}")
1913
+ }
1914
+
1887
1915
  _agent_run_skill() {
1888
1916
  local skill="$1"
1889
1917
  local agent; agent=$(_project_agent)
1890
1918
  local skill_file="${ROLL_HOME}/skills/${skill}/SKILL.md"
1891
1919
  [[ -f "$skill_file" ]] || { err "Skill not found: ${skill}"; return 1; }
1892
1920
  local content; content=$(_skill_content "$skill_file")
1893
- case "$agent" in
1894
- claude) claude -p --output-format text "$content" ;;
1895
- kimi) kimi --quiet -p "$content" ;;
1896
- deepseek) deepseek "$content" ;;
1897
- pi) pi -p "$content" ;;
1898
- codex) codex exec "$content" ;;
1899
- opencode) opencode run "$content" ;;
1900
- *) err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"; return 1 ;;
1901
- esac
1921
+ _agent_argv "$agent" text "$content" || {
1922
+ err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
1923
+ return 1
1924
+ }
1925
+ "${_AGENT_ARGV[@]}"
1902
1926
  }
1903
1927
 
1904
1928
  cmd_review_pr() {
@@ -1958,15 +1982,8 @@ cmd_review_pr() {
1958
1982
  local agent; agent=$(_project_agent)
1959
1983
  local output
1960
1984
  info "Reviewing PR #${pr_number} with ${agent}..."
1961
- case "$agent" in
1962
- claude) output=$(claude -p --output-format text "$prompt" 2>/dev/null) ;;
1963
- kimi) output=$(kimi --quiet -p "$prompt" 2>/dev/null) ;;
1964
- deepseek) output=$(deepseek "$prompt" 2>/dev/null) ;;
1965
- pi) output=$(pi -p "$prompt" 2>/dev/null) ;;
1966
- codex) output=$(codex exec "$prompt" 2>/dev/null) ;;
1967
- opencode) output=$(opencode run "$prompt" 2>/dev/null) ;;
1968
- *) err "Unknown agent '${agent}'"; return 1 ;;
1969
- esac
1985
+ _agent_argv "$agent" text "$prompt" || { err "Unknown agent '${agent}'"; return 1; }
1986
+ output=$("${_AGENT_ARGV[@]}" 2>/dev/null)
1970
1987
 
1971
1988
  echo "$output"
1972
1989
 
@@ -2046,9 +2063,9 @@ cmd_agent() {
2046
2063
  # ═══════════════════════════════════════════════════════════════════════════════
2047
2064
 
2048
2065
  _LOOP_TAG="# roll-loop"
2049
- _SHARED_ROOT="${HOME}/.shared/roll"
2050
- _LOOP_STATE="${HOME}/.shared/roll/loop/state.yaml"
2051
- _LOOP_ALERT="${HOME}/.shared/roll/loop/ALERT.md"
2066
+ : "${_SHARED_ROOT:=${HOME}/.shared/roll}"
2067
+ : "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state.yaml}"
2068
+ : "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT.md}"
2052
2069
  _LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
2053
2070
  _LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
2054
2071
  _LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
@@ -2168,7 +2185,11 @@ _write_loop_runner_script() {
2168
2185
  # stream-json enables realtime streaming; loop-fmt.py humanizes the events.
2169
2186
  local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
2170
2187
  local roll_bin="${ROLL_PKG_DIR}/bin/roll"
2171
- local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
2188
+ # FIX-041: loop cycle is autonomous — permission prompts and sandbox path
2189
+ # restrictions only cause the cycle to burn turns asking for approvals
2190
+ # it cannot receive. Bypass all permission checks for the inner claude
2191
+ # invocation. Worktree isolation contains the blast radius.
2192
+ local cmd_verbose="${cmd/claude -p/claude -p --verbose --dangerously-skip-permissions --output-format stream-json}"
2172
2193
  # US-AUTO-037: strip leading `cd "<path>" && ` (callers like
2173
2194
  # _install_launchd_plists prepend it). The runner now manages cwd itself
2174
2195
  # — pointing at the worktree when isolation succeeds, project_path otherwise.
@@ -2220,6 +2241,35 @@ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
2220
2241
  BRANCH="loop/cycle-\${CYCLE_ID}"
2221
2242
  _USE_WORKTREE=0
2222
2243
  cd "${project_path}" 2>/dev/null || true
2244
+ # FIX-040: orphan worktree recovery — scan for worktrees left by previous failed
2245
+ # cycles (publish failed or inner script was SIGKILL'd). Attempt to publish each
2246
+ # before starting the new cycle. Glob is chronological via timestamp in name.
2247
+ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
2248
+ [ -d "\$_orphan_wt" ] || continue
2249
+ # Confirm it's a real worktree directory (not glob literal when no matches)
2250
+ [ -d "\${_orphan_wt}/.git" ] || [ -f "\${_orphan_wt}/.git" ] || continue
2251
+ _orphan_branch=\$(cd "\$_orphan_wt" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
2252
+ [ -z "\$_orphan_branch" ] && continue
2253
+ _orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
2254
+ if [ "\$_orphan_commits" -gt 0 ]; then
2255
+ echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
2256
+ _orphan_ok=0
2257
+ if ( cd "\$_orphan_wt" && _loop_is_doc_only_change ); then
2258
+ ( cd "\$_orphan_wt" && _loop_publish_doc_pr "\$_orphan_branch" "doc: recover orphan \${_orphan_branch}" ) && _orphan_ok=1
2259
+ else
2260
+ ( cd "\$_orphan_wt" && _loop_publish_pr "\$_orphan_branch" "recover orphan \${_orphan_branch}" ) && _orphan_ok=1
2261
+ fi
2262
+ if [ "\$_orphan_ok" -eq 1 ]; then
2263
+ _worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
2264
+ echo "[loop] FIX-040: orphan recovered and cleaned: \$_orphan_branch"
2265
+ else
2266
+ echo "[loop] FIX-040: orphan recovery publish failed for \$_orphan_branch — leaving preserved"
2267
+ fi
2268
+ else
2269
+ echo "[loop] FIX-040: orphan worktree \$_orphan_wt has no commits; cleaning up"
2270
+ _worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
2271
+ fi
2272
+ done
2223
2273
  # US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
2224
2274
  # post-claude cleanup can diff and delete only this session's additions.
2225
2275
  CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
@@ -2229,8 +2279,12 @@ if _worktree_fetch_origin main \\
2229
2279
  _worktree_submodule_init "\$WT" 2>/dev/null || true
2230
2280
  echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
2231
2281
  else
2232
- echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; running in main tree (no isolation)"
2233
- WT="${project_path}"
2282
+ # P3 fix: skip the cycle entirely when worktree isolation fails.
2283
+ # --dangerously-skip-permissions is only safe paired with worktree isolation;
2284
+ # falling back to the main tree without isolation is unacceptable.
2285
+ _worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
2286
+ echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
2287
+ exit 0
2234
2288
  fi
2235
2289
 
2236
2290
  FMT="${fmt_script}"
@@ -2282,12 +2336,34 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
2282
2336
  _worktree_cleanup "\$WT" "\$BRANCH"
2283
2337
  echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
2284
2338
  else
2285
- _worktree_alert "cycle \${CYCLE_ID}: gh unavailable AND merge_back failed; worktree preserved at \$WT"
2286
- echo "[loop] cycle \${CYCLE_ID}: gh+merge_back both failed; worktree preserved at \$WT"
2339
+ # FIX-039: gh unavailable + merge_back failed push orphan branch+tag to origin
2340
+ # as final safety net so code is never local-only before worktree cleanup.
2341
+ _orphan_tag="loop-orphan-\${CYCLE_ID}"
2342
+ if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
2343
+ && git tag "\$_orphan_tag" 2>/dev/null \
2344
+ && git push origin "\$_orphan_tag" 2>/dev/null ); then
2345
+ _worktree_cleanup "\$WT" "\$BRANCH"
2346
+ _worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
2347
+ echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
2348
+ else
2349
+ _worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
2350
+ echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
2351
+ fi
2287
2352
  fi
2288
2353
  else
2289
- _worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
2290
- echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
2354
+ # FIX-039: PR publish failed push orphan branch+tag to origin as safety net.
2355
+ # (_loop_publish_pr may have already pushed the branch; git push is idempotent.)
2356
+ _orphan_tag="loop-orphan-\${CYCLE_ID}"
2357
+ if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
2358
+ && git tag "\$_orphan_tag" 2>/dev/null \
2359
+ && git push origin "\$_orphan_tag" 2>/dev/null ); then
2360
+ _worktree_cleanup "\$WT" "\$BRANCH"
2361
+ _worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
2362
+ echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
2363
+ else
2364
+ _worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
2365
+ echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
2366
+ fi
2291
2367
  fi
2292
2368
  fi
2293
2369
  else
@@ -2379,7 +2455,7 @@ fi
2379
2455
  echo "\$\$" > "\$LOCK"
2380
2456
  trap 'rm -f "\$LOCK"' EXIT
2381
2457
  if command -v tmux >/dev/null 2>&1; then
2382
- tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-" | while read _s; do
2458
+ tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-${slug}\$" | while read _s; do
2383
2459
  tmux kill-session -t "\$_s" 2>/dev/null || true
2384
2460
  done
2385
2461
  tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
@@ -2520,17 +2596,24 @@ _install_launchd_plists() {
2520
2596
  _agent_skill_cmd() {
2521
2597
  local skill_path="$1"
2522
2598
  local agent; agent=$(_project_agent)
2523
- # Strip YAML frontmatter inline for cron commands
2524
2599
  local strip="awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' '${skill_path}'"
2525
- case "$agent" in
2526
- claude) local _bin; _bin=$(command -v claude 2>/dev/null || echo "claude"); echo "${_bin} -p \"\$(${strip})\"" ;;
2527
- kimi) echo "kimi --quiet -p \"\$(${strip})\"" ;;
2528
- deepseek) echo "deepseek \"\$(${strip})\"" ;;
2529
- pi) echo "pi -p \"\$(${strip})\"" ;;
2530
- codex) echo "codex exec \"\$(${strip})\"" ;;
2531
- opencode) echo "opencode run \"\$(${strip})\"" ;;
2532
- *) err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"; return 1 ;;
2533
- esac
2600
+ _agent_argv "$agent" plain "__PROMPT__" || {
2601
+ err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
2602
+ return 1
2603
+ }
2604
+ # Cron-installed skills (dream / brief / loop) run autonomously and need to
2605
+ # Edit files (docs/dream/, docs/briefs/, BACKLOG, etc.). Claude Code 2.1.x's
2606
+ # pre-write approval UX silently blocks `claude -p` from applying edits in
2607
+ # non-interactive pipe mode bypass it for the cron context.
2608
+ _agent_bypass_claude_perms
2609
+ # In cron context, use absolute claude path so a fresh shell can find it.
2610
+ [[ "$agent" == "claude" ]] && _AGENT_ARGV[0]="$(command -v claude 2>/dev/null || echo claude)"
2611
+ # Drop the prompt sentinel (always last), re-emit head args + quoted $(strip).
2612
+ local out="${_AGENT_ARGV[0]}" i prompt_idx=$((${#_AGENT_ARGV[@]} - 1))
2613
+ for ((i = 1; i < prompt_idx; i++)); do
2614
+ out+=" ${_AGENT_ARGV[i]}"
2615
+ done
2616
+ echo "${out} \"\$(${strip})\""
2534
2617
  }
2535
2618
 
2536
2619
  cmd_loop() {
@@ -2651,15 +2734,46 @@ _loop_off() {
2651
2734
  ok "Loop disabled 已停用"
2652
2735
  }
2653
2736
 
2737
+ _loop_is_active() {
2738
+ # Three-level liveness probe used by FIX-037 heal and `roll loop now`.
2739
+ # Returns 0 if any signal says the cycle is alive, 1 if all signals are dead.
2740
+ # Heartbeat is primary (FIX-038); LOCK PID and tmux session are fallbacks.
2741
+ # ROLL_HEARTBEAT_TIMEOUT (default 1800s) matches the outer heredoc's threshold.
2742
+ local slug="${1:?slug required}"
2743
+ local timeout="${ROLL_HEARTBEAT_TIMEOUT:-1800}"
2744
+ local hb_file="${_SHARED_ROOT}/loop/.heartbeat-${slug}"
2745
+ if [[ -f "$hb_file" ]]; then
2746
+ local ts; ts=$(cat "$hb_file" 2>/dev/null || echo "")
2747
+ if [[ "$ts" =~ ^[0-9]+$ ]]; then
2748
+ local age=$(( $(date -u +%s) - ts ))
2749
+ [[ $age -lt $timeout ]] && return 0
2750
+ fi
2751
+ fi
2752
+ local lock="${_SHARED_ROOT}/loop/.LOCK-${slug}"
2753
+ if [[ -f "$lock" ]]; then
2754
+ local pid; pid=$(head -1 "$lock" 2>/dev/null || echo "")
2755
+ [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && return 0
2756
+ fi
2757
+ command -v tmux >/dev/null 2>&1 && tmux has-session -t "roll-loop-${slug}" 2>/dev/null && return 0
2758
+ return 1
2759
+ }
2760
+
2654
2761
  _loop_now() {
2762
+ local project_path; project_path=$(pwd -P)
2763
+ local slug; slug=$(_project_slug "$project_path")
2764
+ # Manual `roll loop now` must not bypass FIX-037 heal: if state says running
2765
+ # but no live signal exists, this trigger is the canonical recovery point.
2655
2766
  if [[ -f "$_LOOP_STATE" ]] && grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
2656
- warn "Loop already running loop 正在运行中"; return 0
2767
+ if _loop_is_active "$slug"; then
2768
+ warn "Loop already running loop 正在运行中"; return 0
2769
+ fi
2770
+ info "Stale running state detected — healing before new cycle 检测到孤儿状态,正在修复..."
2771
+ printf "status: idle\n" > "$_LOOP_STATE"
2772
+ rm -f "${_SHARED_ROOT}/loop/.LOCK-${slug}" 2>/dev/null || true
2657
2773
  fi
2658
2774
  # Invoke the SAME runner script that launchd would invoke — same tmux,
2659
2775
  # same --verbose, same LOCK, same auto-attach popup. ROLL_LOOP_FORCE
2660
2776
  # bypasses only the active-window check (manual triggers aren't time-gated).
2661
- local project_path; project_path=$(pwd -P)
2662
- local slug; slug=$(_project_slug "$project_path")
2663
2777
  local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
2664
2778
  if [[ ! -f "$runner" ]]; then
2665
2779
  err "Runner script not found: ${runner}"
@@ -2832,6 +2946,7 @@ _loop_reset() {
2832
2946
  else
2833
2947
  info "No loop state to clear 无 loop 状态可清除"
2834
2948
  fi
2949
+ rm -rf "$(_loop_heal_dir)"
2835
2950
  }
2836
2951
 
2837
2952
  # Suppress the auto-attach popup. When the marker file exists, runner scripts
@@ -3223,6 +3338,42 @@ EOF
3223
3338
  return 1
3224
3339
  }
3225
3340
 
3341
+ _loop_heal_dir() {
3342
+ printf '%s\n' "${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/heal"
3343
+ }
3344
+
3345
+ # Bounded CI self-heal gate. Called by the loop SKILL when the
3346
+ # post-build CI check goes red. Counter is per-story, persisted under
3347
+ # $ROLL_LOOP_DIR/heal/<story-id>.count, so retries survive cycle boundaries.
3348
+ #
3349
+ # Exit 0: another heal attempt is allowed (counter incremented). Caller should
3350
+ # invoke roll-fix with the failure summary, then re-run the CI gate.
3351
+ # Exit 1: heal disabled (ROLL_LOOP_NO_HEAL=1) or exhausted (>= ROLL_LOOP_HEAL_MAX).
3352
+ # Caller should write ALERT and stop.
3353
+ _loop_self_heal_ci() {
3354
+ local story_id="$1"
3355
+ [[ -z "$story_id" ]] && return 1
3356
+ [[ "${ROLL_LOOP_NO_HEAL:-0}" == "1" ]] && return 1
3357
+ local max="${ROLL_LOOP_HEAL_MAX:-2}"
3358
+ local dir; dir=$(_loop_heal_dir)
3359
+ local counter="${dir}/${story_id}.count"
3360
+ mkdir -p "$dir"
3361
+ local current; current=$(<"$counter") 2>/dev/null || current=0
3362
+ [[ "$current" =~ ^[0-9]+$ ]] || current=0
3363
+ [[ "$current" -ge "$max" ]] && return 1
3364
+ echo $((current + 1)) > "$counter"
3365
+ return 0
3366
+ }
3367
+
3368
+ # Reset per-story heal counter. Called when CI eventually turns green or by
3369
+ # `roll loop reset`. Idempotent.
3370
+ _loop_clear_heal_state() {
3371
+ local story_id="$1"
3372
+ [[ -z "$story_id" ]] && return 0
3373
+ rm -f "$(_loop_heal_dir)/${story_id}.count" 2>/dev/null
3374
+ return 0
3375
+ }
3376
+
3226
3377
  # Verify TCR rhythm after a story completes. Returns 0 if ok, 1 if no TCR commits.
3227
3378
  # On failure: reverts story in BACKLOG.md to 📋 Todo and writes ALERT.
3228
3379
  _loop_enforce_tcr() {
@@ -4245,16 +4396,6 @@ p.write_text("".join(out))
4245
4396
  PYEOF
4246
4397
  }
4247
4398
 
4248
- cmd_release() {
4249
- local pkg; pkg=$(node -p "require('./package.json').name" 2>/dev/null || true)
4250
- if [[ "$pkg" == "@seanyao/roll" ]]; then
4251
- err "roll 自身发版请用 scripts/release.sh(npm publish 需要人工 2FA)"
4252
- echo " Run: scripts/release.sh"
4253
- exit 1
4254
- fi
4255
- _agent_run_skill "roll-release"
4256
- }
4257
-
4258
4399
  # ═══════════════════════════════════════════════════════════════════════════════
4259
4400
  # BACKLOG — show pending tasks / manage status
4260
4401
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -4951,7 +5092,6 @@ usage() {
4951
5092
  echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
4952
5093
  echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
4953
5094
  echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
4954
- echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
4955
5095
  echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
4956
5096
  echo " review-pr <number> [PR Review] AI-powered code review for a PR AI 代码评审"
4957
5097
  echo ""
@@ -4983,7 +5123,6 @@ main() {
4983
5123
  backlog) cmd_backlog "$@" ;;
4984
5124
  alert) cmd_alert "$@" ;;
4985
5125
  agent) cmd_agent "$@" ;;
4986
- release) cmd_release "$@" ;;
4987
5126
  ci) cmd_ci "$@" ;;
4988
5127
  review-pr) cmd_review_pr "$@" ;;
4989
5128
  version|--version|-v) echo "roll v${VERSION}" ;;
@@ -50,7 +50,18 @@
50
50
  `depends-on:` and `manual-only:` functional tags are allowed; `Domain:` annotation tags are not.
51
51
  Technical details and AC go in `docs/features/`.
52
52
  A well-written BACKLOG description can be used directly as a CHANGELOG entry.
53
+ - **Convention layering**: project-level convention files extend the global SOT — see §9 below.
53
54
  - **Done**: Push + CI passes + deployed. Local-only is not done.
55
+ - **Post-push verification** (universal — applies to any push to main, regardless of which
56
+ skill drove the work):
57
+ - After every push, wait for the triggered CI run and verify status (`gh run watch`
58
+ or equivalent). Do not move on, switch tasks, or claim completion until CI is green.
59
+ - Before pushing any new code commit, verify the **previous** code-changing push's CI
60
+ is green. Never stack new code commits on top of a red CI (this is the failure
61
+ mode FIX-026 / `_loop_precheck_ci` exists to prevent for the loop — humans need
62
+ the same discipline). docs-only commits (matching CI `paths-ignore`) don't reset
63
+ the gate either way.
64
+ - If CI is red, the next action is **fix or revert**, not "queue something else".
54
65
  - **Commit message format**:
55
66
  - Format: `<type>: <description>` (Git Hook may auto-prepend type prefix)
56
67
  - Types: `Story N`, `Fix`, `Refactor`, `Docs`, `Chore`
@@ -93,3 +104,41 @@ Confirm each phase clean before proceeding to the next.
93
104
  - **Story details**: `docs/features/` — AC, implementation specs, dependencies
94
105
  - **Design decisions**: `docs/domain/` — DDD models, architecture records
95
106
  - When `docs/domain/` or `docs/features/` don't exist yet, run `$roll-doc` to bootstrap.
107
+
108
+ ## 9. Convention Architecture
109
+
110
+ Roll conventions form a two-layer hierarchy. The **global** layer is the single
111
+ source of truth for **cross-project rules** — rules that apply regardless of
112
+ stack, language, or domain (e.g., BACKLOG row format, identity, TCR rhythm,
113
+ commit message format, scope discipline). The **project** layer carries only
114
+ **project-specific** rules — stack, structure, build commands, domain
115
+ conventions, deploy targets.
116
+
117
+ **The contract:**
118
+
119
+ 1. Every project-level convention file **must declare it extends the global
120
+ counterpart** via a one-line foundation note at the top (e.g., "Extends
121
+ `~/.<agent>/AGENTS.md`" or "Extends AGENTS.md in this directory").
122
+ 2. Project-level files **never duplicate or re-state** cross-project rules.
123
+ When you find yourself wanting to copy a rule down, add a pointer instead.
124
+ 3. Cross-project rules go in `conventions/global/`. Project-specific rules go
125
+ in `conventions/templates/<type>/`. Anything that applies regardless of
126
+ stack belongs upstairs.
127
+
128
+ **Layered file pairs** — each global ↔ project pair must follow the contract:
129
+
130
+ | Global SOT | Project layer | Audience |
131
+ |---|---|---|
132
+ | `conventions/global/AGENTS.md` | `conventions/templates/<type>/AGENTS.md` | All agents |
133
+ | `conventions/global/CLAUDE.md` | `conventions/templates/<type>/CLAUDE.md` | Claude Code |
134
+ | `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` | Gemini CLI |
135
+ | `conventions/global/project_rules.md` | `conventions/templates/<type>/project_rules.md` | Trae IDE |
136
+
137
+ The CLAUDE / GEMINI / project_rules global files themselves declare they
138
+ extend this AGENTS.md, so this section's rules apply transitively to all
139
+ four file families.
140
+
141
+ **Why it matters**: copies drift, pointers don't. When a rule must change, it
142
+ changes in one place and propagates; if it's duplicated, half the copies get
143
+ updated and the others silently lag — which is the failure mode that produced
144
+ this section in the first place.
@@ -1,6 +1,11 @@
1
1
  # Project Conventions — Backend Service
2
2
 
3
3
  > Reference for skills to infer backend project conventions.
4
+ >
5
+ > **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
6
+ > (installed by `roll setup`). For BACKLOG row format, identity, TCR
7
+ > rhythm, and other cross-project rules, see that file's §4. Only
8
+ > project-specific stack / structure / domain rules live below.
4
9
 
5
10
  ## 1. Design
6
11
  - **API**: RESTful `/api/{res}/{id}`. Structured JSON errors.
@@ -1,6 +1,11 @@
1
1
  # Project Conventions — CLI Tool
2
2
 
3
3
  > Reference for skills to infer CLI project conventions.
4
+ >
5
+ > **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
6
+ > (installed by `roll setup`). For BACKLOG row format, identity, TCR
7
+ > rhythm, and other cross-project rules, see that file's §4. Only
8
+ > project-specific stack / structure / domain rules live below.
4
9
 
5
10
  ## 1. Principles
6
11
  - **Lightweight**: No server/frontend.
@@ -1,6 +1,11 @@
1
1
  # Project Conventions — Frontend Only
2
2
 
3
3
  > Reference for skills to infer frontend project conventions.
4
+ >
5
+ > **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
6
+ > (installed by `roll setup`). For BACKLOG row format, identity, TCR
7
+ > rhythm, and other cross-project rules, see that file's §4. Only
8
+ > project-specific stack / structure / domain rules live below.
4
9
 
5
10
  ## 1. Stack
6
11
  - **Core**: React 18+ / TS / Vite / Tailwind / shadcn/ui.
@@ -1,6 +1,11 @@
1
1
  # Project Conventions — Fullstack Web
2
2
 
3
3
  > Reference for skills to infer fullstack project conventions.
4
+ >
5
+ > **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
6
+ > (installed by `roll setup`). For BACKLOG row format, identity, TCR
7
+ > rhythm, and other cross-project rules, see that file's §4. Only
8
+ > project-specific stack / structure / domain rules live below.
4
9
 
5
10
  ## 1. Stack
6
11
  - **Frontend**: React 18+ / TS / Vite / Tailwind / shadcn/ui.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.516.1",
3
+ "version": "2026.517.2",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -19,7 +19,7 @@ After successful Build & Deploy, extracts completed Stories from BACKLOG.md to g
19
19
 
20
20
  - Generating commit messages or PR descriptions — this skill only runs post-deploy
21
21
  - Recording dev diary / moments (use `$roll-notes`)
22
- - Bumping package version (use `$roll-release`)
22
+ - Bumping package version (use the project's release script, e.g. `scripts/release.sh`)
23
23
 
24
24
  ## Workflow
25
25
 
@@ -357,3 +357,75 @@ After successful deploy in `$roll-build` / `$roll-fix`:
357
357
  ```
358
358
 
359
359
  不需要 `**Added**` / `**Fixed**` 前缀,分组标题已经承担了语义分类的职责。
360
+
361
+ ## 8. features.md 重写模式(产品 SOT)
362
+
363
+ US-DOC-008 — `scripts/release.sh` 在 changelog/release-notes 生成完后会再
364
+ 调一次本 skill,请求"整体重写 `docs/features.md`"。这次调用的语义和上面
365
+ 两种完全不同:**不是基于本版 Story 增量**,而是基于**项目整体当前状态**。
366
+
367
+ ### 8.1 何时触发
368
+
369
+ release.sh 完成 changelog/release-notes 写盘后,喂一段以
370
+ `## 当前任务:重写 docs/features.md(Section 8)` 开头的 prompt。
371
+
372
+ ### 8.2 输入
373
+
374
+ prompt 会包含:
375
+ - 当前 `docs/features.md`(可能为空,可能上一版本的)
376
+ - 当前 `BACKLOG.md` 全文(Epic / Feature 分组结构)
377
+ - 当前 `docs/features/` 目录清单
378
+ - 当前版本号
379
+
380
+ ### 8.3 输出契约
381
+
382
+ 把整个 `docs/features.md` 写出来。结构固定为三段:
383
+
384
+ ```
385
+ # Roll — Features
386
+
387
+ > 说明段(保留原文)
388
+
389
+ ---
390
+
391
+ ## ✨ Core Highlights
392
+
393
+ - **<Feature 名>** — 1 句话产品级描述
394
+ - **<Feature 名>** — 1 句话产品级描述
395
+ - ...(3-5 条)
396
+
397
+ ---
398
+
399
+ ## Features by Epic
400
+
401
+ ### <Epic 名>
402
+ - [<Feature 名>](docs/features/<file>.md) — 1 句话描述
403
+ - <Feature 名> — 1 句话描述(缺 deep doc 时不加链接)
404
+
405
+ ### <Epic 名>
406
+ - ...
407
+
408
+ ---
409
+
410
+ ## 维护说明(保留原文)
411
+ ```
412
+
413
+ ### 8.4 规则
414
+
415
+ - **Catalog 必须列出 BACKLOG 中所有 `### Feature:` 出现的 Feature 名**
416
+ (即使没有 deep doc 也要列)
417
+ - Feature 名跟 `docs/features/<file>.md` 文件名一致时,加链接到该 md
418
+ - 没有对应 deep doc 的 Feature,**只写 plain text 不加链接**
419
+ - 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
420
+ - 分组用 BACKLOG 的 Epic 名,原序,不重排
421
+ - Core Highlights 从所有 Features 里挑 3-5 个最能代表产品定位的,
422
+ 描述用 bold 标 Feature 名后接说明;不照搬 catalog 文案
423
+ - **不**写 "Recent Activity" 类区块——features.md 是 SOT,全量当下状态
424
+ - **不**写版本号、不引用 changelog 条目
425
+ - 说明段(顶部 quote)和维护说明(尾部)原文保留,不要重新生成措辞
426
+
427
+ ### 8.5 失败安全
428
+
429
+ 如果 prompt 信息不足(BACKLOG 解析失败等),**不要部分写入** —— 输出原
430
+ 文件内容即可。release.sh 会捕获 stdout 后比较:内容未变就不 stage。
431
+
@@ -17,7 +17,7 @@ description: |
17
17
  > Common Sense defined in the project AGENTS.md.
18
18
 
19
19
  Owner-facing digest of autonomous agent activity. Gives the human everything
20
- needed to decide whether to run `roll-release` — without having to read every
20
+ needed to decide whether to cut a new release — without having to read every
21
21
  commit or diff.
22
22
 
23
23
  ## Distinct from roll-.changelog
@@ -7,7 +7,7 @@ description: |
7
7
  Actions), scans BACKLOG.md for 📋 Todo items, and routes each to the
8
8
  appropriate skill: US-XXX → $roll-build, FIX-XXX → $roll-fix,
9
9
  REFACTOR-XXX → $roll-build. Handles agent fallback on token/network failure.
10
- Never executes roll-release autonomously — release is always a human decision.
10
+ Never cuts a release autonomously — release is always a human decision.
11
11
  Triggers roll-brief when a Feature completes.
12
12
  ---
13
13
 
@@ -18,7 +18,7 @@ description: |
18
18
 
19
19
  Runs on a schedule. Picks up pending BACKLOG items and executes them without
20
20
  human intervention. The human stays informed via `roll-brief` and retains
21
- sole authority over `roll-release`.
21
+ sole authority over releases.
22
22
 
23
23
  ## Execution Boundary
24
24
 
@@ -28,7 +28,7 @@ sole authority over `roll-release`.
28
28
  - REFACTOR-XXX (Refactors) → `$roll-build`
29
29
 
30
30
  **What roll-loop never executes:**
31
- - `roll-release` — production deployment is always a human decision
31
+ - Releases — production deployment is always a human decision (requires 2FA in real terminal)
32
32
  - Any Story marked 🚫 Hold or flagged for human review
33
33
  - Destructive operations outside normal skill scope
34
34
 
@@ -37,6 +37,30 @@ sole authority over `roll-release`.
37
37
  故事评审等场景)。loop 通过 LOCK 和 `🔨 In Progress` 状态识别并跳过人正在做的故事,
38
38
  人机并行不会撞车(见 Concurrency Safety)。
39
39
 
40
+ ## Environment Constraints (autonomous loop)
41
+
42
+ You are running inside an autonomous cycle. No human is watching this turn.
43
+ Adapt commands to the constraints below — otherwise you will burn turns on
44
+ denied operations and the cycle will idle-exit.
45
+
46
+ - **No `AskUserQuestion`**: no human can answer. If you genuinely cannot
47
+ proceed without a decision, write an entry to `${HOME}/.shared/roll/loop/ALERT.md`
48
+ describing what's needed and exit cleanly.
49
+ - **Avoid compound bash**: each `Bash` call must run a single command.
50
+ No `cmd1 && cmd2`, no `cmd1 ; cmd2`, no pipes (`|`), no `$(...)` /
51
+ backtick subshells, no `bash -c '...'` with nested quoting. These are
52
+ rejected by static analysis before they run. Chain operations as
53
+ separate Bash calls and read intermediate output yourself.
54
+ - **Prefer Read/Edit over cat/sed**: use the `Read` tool for any file
55
+ lookup, `Edit` for modifications. They cross sandbox boundaries that
56
+ `cat` / `ls` / `sed` cannot.
57
+ - **CWD-relative paths first**: the cycle's CWD is the per-cycle worktree.
58
+ Files inside it (BACKLOG.md, bin/roll, tests/, docs/) are always
59
+ accessible. Files at `~/.shared/roll/...` are reachable via the `Read`
60
+ tool but not via shell commands.
61
+ - **Skill invocation is the work**: route US/REFACTOR via `$roll-build`,
62
+ FIX via `$roll-fix`. Do not try to re-implement those flows inline.
63
+
40
64
  ## Configuration
41
65
 
42
66
  ```yaml
@@ -208,12 +232,44 @@ After each item completes:
208
232
  it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
209
233
  is required to work through `~/.ssh/config` host rewrites that break gh's
210
234
  auto-detection.
211
- - CI passes → continue normally
212
- - CI fails / times out / `gh` call fails → keep story as `🔨 In Progress`
213
- (do NOT mark Done); write ALERT; skip to next story
235
+ - CI passes → call `_loop_clear_heal_state <story_id>` (idempotent) and
236
+ continue normally
237
+ - CI fails / times out / `gh` call fails → enter **CI self-heal** (US-AUTO-041)
214
238
  - `gh` binary not installed (`command -v gh` fails) → skip gracefully
215
239
  (return 0). Any other `gh` error is **not** "gh unavailable" — it is a
216
240
  hard failure and must block the gate.
241
+
242
+ **CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT:
243
+
244
+ ```
245
+ shell: _loop_self_heal_ci <story_id>
246
+ ├── exit 0 → heal attempt allowed (counter incremented)
247
+ │ 1. Capture failure summary:
248
+ │ gh run view --log-failed --repo <slug> $(gh run list --commit HEAD \
249
+ │ --json databaseId,conclusion -L 5 | jq -r '.[] | select(.conclusion=="failure") | .databaseId' | head -1) \
250
+ │ 2>/dev/null | head -200 > /tmp/roll-heal-<story_id>.log
251
+ │ 2. Invoke Skill("roll-fix") with brief:
252
+ │ "CI red after <story_id>. Failing run logs at /tmp/roll-heal-<story_id>.log.
253
+ │ Diagnose root cause, fix via TCR, commit, push. Do NOT change <story_id>'s
254
+ │ BACKLOG status — it stays ✅ Done. The fix is a follow-up."
255
+ │ 3. After roll-fix completes, return to step 2 (CI Gate) — re-run
256
+ │ `roll ci --wait`. The counter prevents infinite loops.
257
+
258
+ └── exit 1 → heal exhausted (>=ROLL_LOOP_HEAL_MAX, default 2) or disabled
259
+ (ROLL_LOOP_NO_HEAL=1):
260
+ 1. Keep story as ✅ Done (commits are already on main — CI red is a
261
+ follow-up problem, not a story-failure)
262
+ 2. Write ALERT to `~/.shared/roll/loop/ALERT.md` with:
263
+ - story ID, time, commit SHA
264
+ - heal attempts made (read counter from
265
+ `${ROLL_LOOP_DIR:-~/.shared/roll/loop}/heal/<story_id>.count`)
266
+ - last failure summary (head of /tmp/roll-heal-<story_id>.log)
267
+ - suggested actions: `$roll-fix` manually / inspect CI / `roll loop reset`
268
+ 3. Skip to next story.
269
+ ```
270
+
271
+ **Bypass for debugging / cost control:** set `ROLL_LOOP_NO_HEAL=1` to restore
272
+ pre-US-AUTO-041 fail-fast behaviour.
217
273
  3. Update state file: `status: idle`
218
274
  4. Check if a Feature is now fully complete (all its Stories ✅)
219
275
  5. If yes and `brief_on_feature_complete: true` → invoke `Skill("roll-brief")`
@@ -1,146 +0,0 @@
1
- ---
2
- name: roll-release
3
- license: MIT
4
- allowed-tools: "Read, Edit, Bash(git:*), Bash(npm:*), Bash(sed:*), Bash(date:*), Bash(gh:*)"
5
- description: "Release skill for roll maintainers. Calculates next version (YYYY.MMDD.N format, auto-increments N from today's git tags), updates VERSION in bin/roll and package.json, commits, tags, and pushes to trigger npm auto-publish via GitHub Actions. Trigger: release, publish, 发版, 发布新版本."
6
- ---
7
-
8
- # Release (roll-release)
9
-
10
- One-command publish flow for roll maintainers.
11
-
12
- ## When Not to Use
13
-
14
- - Non-maintainer users (this skill publishes the package defined in `package.json` — confirm scope before running)
15
- - Internal project releases — only for the `roll` CLI package itself
16
- - Hotfixing code without a version bump (use `$roll-fix`)
17
- - Generating user-facing release notes (use `$roll-.changelog`)
18
-
19
- ## Version Format
20
-
21
- `YYYY.MMDD.N` — e.g. `2026.419.1`
22
-
23
- - `YYYY.MMDD` = today's date, month has **no leading zero** (e.g. `420` not `0420`)
24
- - `N` = auto-incremented from existing git tags for today (starts at 1)
25
-
26
- ## Execution Steps
27
-
28
- ### Step 1: Calculate Version
29
-
30
- First, inspect recent tags to confirm the actual format in use:
31
-
32
- ```bash
33
- git tag | sort -V | tail -5
34
- ```
35
-
36
- Then calculate:
37
-
38
- ```bash
39
- today=$(date +%Y.%-m%d)
40
- last_n=$(git tag | grep "^v${today}\." | sed "s/^v${today}\.//" | sort -n | tail -1)
41
-
42
- # Reuse tag if latest today's tag already points to HEAD (e.g. npm publish failed, retrying)
43
- if [[ -n "$last_n" ]]; then
44
- last_tag="v${today}.${last_n}"
45
- if [[ "$(git rev-list -n1 "$last_tag")" == "$(git rev-parse HEAD)" ]]; then
46
- version="${today}.${last_n}"
47
- echo "♻️ Reusing ${last_tag} — same commit, skipping version bump"
48
- # Skip Steps 2-4, jump directly to Step 5 (GitHub Release) or Step 6 (npm publish)
49
- else
50
- n=$(( last_n + 1 ))
51
- version="${today}.${n}"
52
- fi
53
- else
54
- version="${today}.1"
55
- fi
56
- ```
57
-
58
- Show the proposed version to the user:
59
- ```
60
- Recent tags: v2026.419.1 v2026.419.2 v2026.420.3
61
- Proposed version: 2026.420.4 ← new version (code changed since last tag)
62
- — or —
63
- ♻️ Reusing v2026.420.3 ← same commit, retry after npm failure
64
- Proceed? [y/N]
65
- ```
66
-
67
- Wait for confirmation before continuing.
68
-
69
- ### Step 2: Update Version Fields
70
-
71
- **`bin/roll`** — update the VERSION line:
72
- ```bash
73
- sed -i '' "s/^VERSION=.*/VERSION=\"${version}\"/" bin/roll
74
- ```
75
-
76
- **`package.json`** — update the version field:
77
- ```bash
78
- npm version "${version}" --no-git-tag-version
79
- ```
80
-
81
- Verify both files show the new version before continuing.
82
-
83
- ### Step 3: Commit
84
-
85
- ```bash
86
- git add bin/roll package.json
87
- git commit -m "[release] v${version}"
88
- ```
89
-
90
- ### Step 4: Tag and Push
91
-
92
- ```bash
93
- git tag "v${version}"
94
- git push && git push --tags
95
- ```
96
-
97
- ### Step 5: Create GitHub Release
98
-
99
- **This step is mandatory.** Without it, `roll` update notifications will not work.
100
-
101
- Convert version to changelog date, extract notes, then create the release:
102
-
103
- ```bash
104
- # Convert version (2026.510.3) to changelog date (2026.05.10)
105
- _year=$(echo "${version}" | cut -d. -f1)
106
- _mmdd=$(echo "${version}" | cut -d. -f2)
107
- if [ ${#_mmdd} -eq 3 ]; then
108
- _cl_date="${_year}.0${_mmdd:0:1}.${_mmdd:1:2}"
109
- else
110
- _cl_date="${_year}.${_mmdd:0:2}.${_mmdd:2:2}"
111
- fi
112
-
113
- # Extract release notes from CHANGELOG.md
114
- notes=$(sed -n "/^## ${_cl_date}$/,/^## /{ /^## ${_cl_date}$/d; /^## /d; p; }" CHANGELOG.md | sed '/^[[:space:]]*$/d')
115
-
116
- gh release create "v${version}" \
117
- --title "v${version}" \
118
- --notes "${notes:-Release v${version}}"
119
- ```
120
-
121
- This enables the background update check in `bin/roll` (`_check_update_async`), which queries the GitHub Releases API.
122
-
123
- ### Step 6: Publish to npm
124
-
125
- ```bash
126
- npm publish --access public
127
- ```
128
-
129
- This will open a browser for 2FA verification. Wait for it to complete before continuing.
130
-
131
- ### Step 7: Confirm
132
-
133
- After publish, show:
134
- ```
135
- ✅ Released v{version}
136
- 🏷 Tag: v{version} pushed to origin
137
- 📦 npm published: {package_name}@{version} # package name read from package.json
138
- 🐙 GitHub Release: https://github.com/{owner}/{repo}/releases/tag/v{version}
139
- 🔗 https://www.npmjs.com/package/{package_name}
140
- ```
141
-
142
- ## Abort Conditions
143
-
144
- - Uncommitted changes in `bin/roll` or `package.json` → warn and abort
145
- - Tag already exists for today's N → increment N and re-propose
146
- - `git push` fails → show error, do not leave a dangling local tag (run `git tag -d v{version}`)