@seanyao/roll 2026.512.1 → 2026.512.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 +8 -0
- package/bin/roll +181 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.512.5
|
|
4
|
+
- **Fixed**: loop 遇到 API 错误时自动重试,不再直接中断
|
|
5
|
+
|
|
6
|
+
## v2026.512.3
|
|
7
|
+
- **Added**: BACKLOG 支持 block / defer / unblock 状态管理 — 标记卡住的任务不再占队列
|
|
8
|
+
- **Fixed**: 自动弹窗现在能识别 Ghostty 和 iTerm2,不再强制弹出 Terminal.app
|
|
9
|
+
- **Fixed**: loop 检测到上一轮还在跑时自动跳过,不重复启动
|
|
10
|
+
|
|
3
11
|
## v2026.512.1
|
|
4
12
|
- **Added**: `roll loop pause` / `roll loop resume` — 想自己上手时一键暂停 loop,做完再恢复
|
|
5
13
|
- **Added**: `roll status` 新增所有项目的 loop 状态一览 — 调度时间、待办数、是否在跑
|
package/bin/roll
CHANGED
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Roll — AI Agent Convention Manager
|
|
5
5
|
# Single source of truth for how all AI coding agents behave.
|
|
6
6
|
|
|
7
|
-
VERSION="2026.512.
|
|
7
|
+
VERSION="2026.512.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"
|
|
@@ -1961,13 +1961,22 @@ _write_loop_runner_script() {
|
|
|
1961
1961
|
local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
|
|
1962
1962
|
cat > "$inner_path" << INNER
|
|
1963
1963
|
#!/bin/bash -l
|
|
1964
|
+
set -o pipefail
|
|
1964
1965
|
export PATH="/opt/homebrew/bin:\$PATH"
|
|
1965
1966
|
FMT="${fmt_script}"
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1967
|
+
for _attempt in 1 2 3; do
|
|
1968
|
+
if [ -f "\$FMT" ]; then
|
|
1969
|
+
( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
|
|
1970
|
+
else
|
|
1971
|
+
( cd "${project_path}" && ${cmd_verbose} )
|
|
1972
|
+
fi
|
|
1973
|
+
_exit=\$?
|
|
1974
|
+
[ "\$_exit" -eq 0 ] && break
|
|
1975
|
+
if [ "\$_attempt" -lt 3 ]; then
|
|
1976
|
+
echo "[loop] claude exited \$_exit (attempt \$_attempt/3) — retrying in 30s..."
|
|
1977
|
+
sleep 30
|
|
1978
|
+
fi
|
|
1979
|
+
done
|
|
1971
1980
|
INNER
|
|
1972
1981
|
chmod +x "$inner_path"
|
|
1973
1982
|
|
|
@@ -1994,6 +2003,13 @@ if [ -f "\$LOCK" ]; then
|
|
|
1994
2003
|
fi
|
|
1995
2004
|
rm -f "\$LOCK"
|
|
1996
2005
|
fi
|
|
2006
|
+
# Guard against stale-LOCK case: if the tmux session is already alive,
|
|
2007
|
+
# a previous runner's LOCK was removed (e.g. parent terminal closed) but
|
|
2008
|
+
# the work is still in progress — don't kill it.
|
|
2009
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "\$SESSION" 2>/dev/null; then
|
|
2010
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] tmux session \$SESSION still active, skipping" >> "\$LOG"
|
|
2011
|
+
exit 0
|
|
2012
|
+
fi
|
|
1997
2013
|
echo "\$\$" > "\$LOCK"
|
|
1998
2014
|
trap 'rm -f "\$LOCK"' EXIT
|
|
1999
2015
|
if command -v tmux >/dev/null 2>&1; then
|
|
@@ -2004,29 +2020,29 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
2004
2020
|
# tmux session so the user can watch the loop work in real time. Best-effort
|
|
2005
2021
|
# focus retention: capture the current frontmost app and re-activate after.
|
|
2006
2022
|
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2023
|
+
# Runtime terminal detection: try preferred first, fallback through installed apps.
|
|
2024
|
+
# open -na returns non-zero when app not found, so || chain works as fallback.
|
|
2025
|
+
_PREF="${terminal_pref}"
|
|
2026
|
+
_launched=0
|
|
2027
|
+
if [ "\$_PREF" = "ghostty" ] || [ "\$_PREF" = "Ghostty" ]; then
|
|
2028
|
+
open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
|
|
2029
|
+
fi
|
|
2030
|
+
if [ "\$_launched" -eq 0 ] && { [ "\$_PREF" = "iTerm2" ] || [ "\$_PREF" = "iTerm" ] || [ -d "/Applications/iTerm.app" ]; }; then
|
|
2031
|
+
osascript \\
|
|
2032
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2033
|
+
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
|
|
2034
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \\
|
|
2035
|
+
&& _launched=1 || true
|
|
2036
|
+
fi
|
|
2037
|
+
if [ "\$_launched" -eq 0 ] && [ -d "/Applications/Ghostty.app" ]; then
|
|
2038
|
+
open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1 && _launched=1 || true
|
|
2039
|
+
fi
|
|
2040
|
+
if [ "\$_launched" -eq 0 ] && command -v osascript >/dev/null 2>&1; then
|
|
2041
|
+
osascript \\
|
|
2042
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2043
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
2044
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
2045
|
+
fi
|
|
2030
2046
|
fi
|
|
2031
2047
|
while tmux has-session -t "\$SESSION" 2>/dev/null; do sleep 5; done
|
|
2032
2048
|
else
|
|
@@ -2799,9 +2815,45 @@ cmd_release() {
|
|
|
2799
2815
|
}
|
|
2800
2816
|
|
|
2801
2817
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2802
|
-
# BACKLOG — show pending tasks
|
|
2818
|
+
# BACKLOG — show pending tasks / manage status
|
|
2803
2819
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2804
2820
|
|
|
2821
|
+
# Update status of all BACKLOG rows whose ID field contains <pattern> (case-insensitive).
|
|
2822
|
+
# Uses Python for reliable emoji/Unicode handling.
|
|
2823
|
+
_backlog_set_status() {
|
|
2824
|
+
local pattern="$1"
|
|
2825
|
+
local new_status="$2"
|
|
2826
|
+
local backlog="BACKLOG.md"
|
|
2827
|
+
python3 -c "
|
|
2828
|
+
import sys, re
|
|
2829
|
+
pattern, new_status, filename = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
2830
|
+
lines = open(filename, encoding='utf-8').readlines()
|
|
2831
|
+
count = 0
|
|
2832
|
+
out = []
|
|
2833
|
+
for line in lines:
|
|
2834
|
+
if line.startswith('|') and line.count('|') >= 4:
|
|
2835
|
+
parts = line.split('|')
|
|
2836
|
+
if len(parts) >= 5:
|
|
2837
|
+
id_field = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', parts[1]).strip()
|
|
2838
|
+
if pattern.upper() in id_field.upper():
|
|
2839
|
+
parts[-2] = ' ' + new_status + ' '
|
|
2840
|
+
line = '|'.join(parts)
|
|
2841
|
+
count += 1
|
|
2842
|
+
out.append(line)
|
|
2843
|
+
open(filename, 'w', encoding='utf-8').writelines(out)
|
|
2844
|
+
print(count)
|
|
2845
|
+
" "$pattern" "$new_status" "$backlog"
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
_backlog_extract_id() {
|
|
2849
|
+
local line="$1"
|
|
2850
|
+
if echo "$line" | grep -q '\[US-'; then
|
|
2851
|
+
echo "$line" | sed 's/.*\[\(US-[^]]*\)\].*/\1/'
|
|
2852
|
+
else
|
|
2853
|
+
echo "$line" | awk -F'|' '{print $2}' | tr -d ' '
|
|
2854
|
+
fi
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2805
2857
|
cmd_backlog() {
|
|
2806
2858
|
local backlog="BACKLOG.md"
|
|
2807
2859
|
if [[ ! -f "$backlog" ]]; then
|
|
@@ -2809,8 +2861,38 @@ cmd_backlog() {
|
|
|
2809
2861
|
return 1
|
|
2810
2862
|
fi
|
|
2811
2863
|
|
|
2812
|
-
local
|
|
2864
|
+
local subcmd="${1:-}"
|
|
2865
|
+
|
|
2866
|
+
# ── Status management subcommands ─────────────────────────────────────────
|
|
2867
|
+
case "$subcmd" in
|
|
2868
|
+
block|defer|unblock|promote)
|
|
2869
|
+
local pattern="${2:-}"
|
|
2870
|
+
local reason="${3:-}"
|
|
2871
|
+
if [[ -z "$pattern" ]]; then
|
|
2872
|
+
err "Usage: roll backlog $subcmd <pattern> [reason] 用法: roll backlog $subcmd <匹配模式> [原因]"
|
|
2873
|
+
return 1
|
|
2874
|
+
fi
|
|
2875
|
+
local new_status
|
|
2876
|
+
case "$subcmd" in
|
|
2877
|
+
block) new_status="🔒 Blocked${reason:+ [${reason}]}" ;;
|
|
2878
|
+
defer) new_status="⏸ Deferred${reason:+ [${reason}]}" ;;
|
|
2879
|
+
unblock|promote) new_status="📋 Todo" ;;
|
|
2880
|
+
esac
|
|
2881
|
+
local count
|
|
2882
|
+
count=$(_backlog_set_status "$pattern" "$new_status")
|
|
2883
|
+
if [[ "$count" -eq 0 ]]; then
|
|
2884
|
+
echo " No items matched: $pattern 未找到匹配项: $pattern"
|
|
2885
|
+
else
|
|
2886
|
+
echo " Updated ${count} item(s) → ${new_status} 已更新 ${count} 条目"
|
|
2887
|
+
fi
|
|
2888
|
+
return
|
|
2889
|
+
;;
|
|
2890
|
+
esac
|
|
2891
|
+
|
|
2892
|
+
# ── Display mode ──────────────────────────────────────────────────────────
|
|
2893
|
+
local DIM='\033[2m'
|
|
2813
2894
|
|
|
2895
|
+
local us_items fix_items refactor_items idea_items total=0
|
|
2814
2896
|
us_items=$(grep -E '^\| \[US-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2815
2897
|
fix_items=$(grep -E '^\| FIX-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2816
2898
|
refactor_items=$(grep -E '^\| REFACTOR-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
@@ -2827,6 +2909,20 @@ cmd_backlog() {
|
|
|
2827
2909
|
[[ -z "$idea_items" ]] && idea_count=0
|
|
2828
2910
|
total=$(( us_count + fix_count + refactor_count + idea_count ))
|
|
2829
2911
|
|
|
2912
|
+
local blocked_items deferred_items unknown_items
|
|
2913
|
+
blocked_items=$(grep -E '^\|' "$backlog" | grep '🔒 Blocked' || true)
|
|
2914
|
+
deferred_items=$(grep -E '^\|' "$backlog" | grep '⏸ Deferred' || true)
|
|
2915
|
+
unknown_items=$( { grep -E '^\| \[US-' "$backlog"; grep -E '^\| FIX-' "$backlog"; grep -E '^\| REFACTOR-' "$backlog"; grep -E '^\| IDEA-' "$backlog"; } \
|
|
2916
|
+
| grep -v '📋 Todo\|🔨 In Progress\|✅ Done\|🔒 Blocked\|⏸ Deferred' || true)
|
|
2917
|
+
|
|
2918
|
+
local blocked_count deferred_count unknown_count
|
|
2919
|
+
blocked_count=$(echo "$blocked_items" | grep -c . || true)
|
|
2920
|
+
deferred_count=$(echo "$deferred_items" | grep -c . || true)
|
|
2921
|
+
unknown_count=$(echo "$unknown_items" | grep -c . || true)
|
|
2922
|
+
[[ -z "$blocked_items" ]] && blocked_count=0
|
|
2923
|
+
[[ -z "$deferred_items" ]] && deferred_count=0
|
|
2924
|
+
[[ -z "$unknown_items" ]] && unknown_count=0
|
|
2925
|
+
|
|
2830
2926
|
echo ""
|
|
2831
2927
|
echo -e " ${BOLD}Pending Backlog 待处理任务${NC} (${total} items)"
|
|
2832
2928
|
echo ""
|
|
@@ -2879,6 +2975,53 @@ cmd_backlog() {
|
|
|
2879
2975
|
echo -e " ${GREEN}✓ Nothing pending — backlog is clear 暂无待处理任务${NC}"
|
|
2880
2976
|
echo ""
|
|
2881
2977
|
fi
|
|
2978
|
+
|
|
2979
|
+
# ── Blocked ───────────────────────────────────────────────────────────────
|
|
2980
|
+
if [[ $blocked_count -gt 0 ]]; then
|
|
2981
|
+
echo -e " ${DIM}Blocked 已阻塞 (${blocked_count})${NC}"
|
|
2982
|
+
while IFS= read -r line; do
|
|
2983
|
+
[[ -z "$line" ]] && continue
|
|
2984
|
+
local id desc reason
|
|
2985
|
+
id=$(_backlog_extract_id "$line")
|
|
2986
|
+
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-52)
|
|
2987
|
+
reason=$(echo "$line" | awk -F'|' '{print $4}' | grep -oE '\[.*\]' | tr -d '[]' || true)
|
|
2988
|
+
printf " ${DIM}🔒 %-14s %s${NC}" "$id" "$desc"
|
|
2989
|
+
[[ -n "$reason" ]] && printf "${DIM} (%s)${NC}" "$reason"
|
|
2990
|
+
printf "\n"
|
|
2991
|
+
done <<< "$blocked_items"
|
|
2992
|
+
echo ""
|
|
2993
|
+
fi
|
|
2994
|
+
|
|
2995
|
+
# ── Deferred ──────────────────────────────────────────────────────────────
|
|
2996
|
+
if [[ $deferred_count -gt 0 ]]; then
|
|
2997
|
+
echo -e " ${DIM}Deferred 已推迟 (${deferred_count})${NC}"
|
|
2998
|
+
while IFS= read -r line; do
|
|
2999
|
+
[[ -z "$line" ]] && continue
|
|
3000
|
+
local id desc reason
|
|
3001
|
+
id=$(_backlog_extract_id "$line")
|
|
3002
|
+
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-52)
|
|
3003
|
+
reason=$(echo "$line" | awk -F'|' '{print $4}' | grep -oE '\[.*\]' | tr -d '[]' || true)
|
|
3004
|
+
printf " ${DIM}⏸ %-14s %s${NC}" "$id" "$desc"
|
|
3005
|
+
[[ -n "$reason" ]] && printf "${DIM} (%s)${NC}" "$reason"
|
|
3006
|
+
printf "\n"
|
|
3007
|
+
done <<< "$deferred_items"
|
|
3008
|
+
echo ""
|
|
3009
|
+
fi
|
|
3010
|
+
|
|
3011
|
+
# ── Unknown status (show for human/AI triage) ─────────────────────────────
|
|
3012
|
+
if [[ $unknown_count -gt 0 ]]; then
|
|
3013
|
+
echo -e " ${YELLOW}? Unknown Status 未知状态 (${unknown_count})${NC}"
|
|
3014
|
+
echo -e " ${YELLOW} Fix: roll backlog block/defer/unblock <pattern> 运行命令修正状态${NC}"
|
|
3015
|
+
while IFS= read -r line; do
|
|
3016
|
+
[[ -z "$line" ]] && continue
|
|
3017
|
+
local id desc status_raw
|
|
3018
|
+
id=$(_backlog_extract_id "$line")
|
|
3019
|
+
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-52)
|
|
3020
|
+
status_raw=$(echo "$line" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//')
|
|
3021
|
+
printf " ${YELLOW}? %-14s %s [%s]${NC}\n" "$id" "$desc" "$status_raw"
|
|
3022
|
+
done <<< "$unknown_items"
|
|
3023
|
+
echo ""
|
|
3024
|
+
fi
|
|
2882
3025
|
}
|
|
2883
3026
|
|
|
2884
3027
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2985,7 +3128,10 @@ usage() {
|
|
|
2985
3128
|
echo " peer [Peer Review] Cross-agent negotiation 跨 Agent 协商对审"
|
|
2986
3129
|
echo " loop <on|off|now|status|monitor|resume|reset> [Autonomous] Manage scheduled BACKLOG executor 管理自主执行循环"
|
|
2987
3130
|
echo " brief [Digest] Show latest owner brief (regenerate if stale) 展示最新简报"
|
|
2988
|
-
echo " backlog [View] Show
|
|
3131
|
+
echo " backlog [View] Show pending tasks (Todo/Blocked/Deferred/Unknown) 显示任务清单"
|
|
3132
|
+
echo " backlog block <pat> [reason] Mark matching items as 🔒 Blocked 标记为已阻塞"
|
|
3133
|
+
echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
|
|
3134
|
+
echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
|
|
2989
3135
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
2990
3136
|
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
2991
3137
|
echo ""
|
|
@@ -2995,7 +3141,9 @@ usage() {
|
|
|
2995
3141
|
echo " roll init # New or re-merge project (run in project) 新建或重新合并(项目目录)"
|
|
2996
3142
|
echo " roll loop on # Enable autonomous loop (cron) 启用自主执行循环"
|
|
2997
3143
|
echo " roll brief # Show latest brief 查看最新简报"
|
|
2998
|
-
echo " roll backlog # Show pending
|
|
3144
|
+
echo " roll backlog # Show pending/blocked/deferred items 查看待处理任务"
|
|
3145
|
+
echo " roll backlog defer US-DOC '过早引入' # Defer all US-DOC-* items 推迟一类任务"
|
|
3146
|
+
echo " roll backlog block US-HW-001 '硬件未到货' # Block a specific item 标记阻塞"
|
|
2999
3147
|
echo " roll agent use kimi # Switch this project to kimi 切换当前项目到 kimi"
|
|
3000
3148
|
|
|
3001
3149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.512.
|
|
3
|
+
"version": "2026.512.5",
|
|
4
4
|
"description": "Roll — Roll out features with AI agents",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "find tests/unit tests/integration -name '*.bats' | sort | xargs ./tests/helpers/bats-core/bin/bats"
|