@seanyao/roll 2026.511.6 → 2026.511.8
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 +23 -21
- package/bin/roll +219 -4
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +70 -41
- package/skills/roll-.dream/SKILL.md +17 -15
- package/skills/roll-loop/SKILL.md +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
- **Fixed**:
|
|
3
|
+
## Unreleased
|
|
4
|
+
- **Fixed**: `roll loop attach` 不再黑屏,AI 干活过程实时可见
|
|
5
5
|
|
|
6
6
|
## v2026.511.7
|
|
7
|
-
- **Added**: loop
|
|
8
|
-
- **Added**: loop
|
|
9
|
-
- **
|
|
7
|
+
- **Added**: loop 跑起来时自动弹出一个终端窗口,看 AI 实时干活
|
|
8
|
+
- **Added**: `roll loop mute` 关掉自动弹窗,`roll loop unmute` 恢复
|
|
9
|
+
- **Added**: `roll loop runs` — 查看 loop 最近几次都跑了什么
|
|
10
|
+
- **Added**: `roll loop attach` — 随时接入正在跑的 loop 现场围观
|
|
11
|
+
- **Added**: BACKLOG 任务执行中会实时显示 🔨 进度,不用等做完才知道
|
|
12
|
+
- **Added**: `roll setup` 自动安装 tmux(macOS 通过 brew)
|
|
13
|
+
- **Improved**: 代码巡检(dream)报告改为中文输出
|
|
14
|
+
- **Fixed**: loop 在某些情况下完成后不正常退出
|
|
15
|
+
- **Fixed**: loop 中途崩溃后下次启动会自动清理残留状态
|
|
10
16
|
|
|
11
17
|
## v2026.511.6
|
|
12
|
-
- **
|
|
13
|
-
- **Added**: roll-loop SKILL 显式声明 skip-🔨 In Progress 语义 — claude 扫 BACKLOG 时跳过已被人工或 peer agent 标记的执行中条目,为人机协同和多 agent 协作奠定基础。
|
|
14
|
-
- **Fixed**: 5 个 pre-existing 测试失败 — `run_roll` helper 切换到 TEST_TMP 作为 cwd 避免 slug 冲突;loop status 测试匹配三态显示新文案;dashboard 测试匹配 `_launchd_svc_state` + array 派生 schedule 的新结构。
|
|
18
|
+
- **Fixed**: 多个 loop 实例不会再因为定时重复触发而互相打架
|
|
15
19
|
|
|
16
20
|
## v2026.511.5
|
|
17
|
-
- **Fixed**:
|
|
18
|
-
- **Improved**: roll loop status
|
|
21
|
+
- **Fixed**: 升级 roll 后 loop 服务自动生效,无需手动重启
|
|
22
|
+
- **Improved**: `roll loop status` 三态显示,看得清是真没装、装了没启、还是在跑
|
|
19
23
|
|
|
20
24
|
## v2026.511.4
|
|
21
|
-
- **Fixed**:
|
|
25
|
+
- **Fixed**: 升级 roll 后 `roll init` 自动迁移 loop 配置,少一步手动操作
|
|
22
26
|
|
|
23
27
|
## v2026.511.3
|
|
24
|
-
- **Fixed**:
|
|
25
|
-
- **Fixed**:
|
|
28
|
+
- **Fixed**: 多个项目同时跑 loop,互不干扰
|
|
29
|
+
- **Fixed**: 在 roll 项目里运行 `roll release` 会提示改用 `scripts/release.sh`
|
|
26
30
|
|
|
27
31
|
## v2026.511.2
|
|
28
|
-
- **Added**: roll loop monitor
|
|
29
|
-
- **Fixed**: dashboard
|
|
30
|
-
- **Fixed**: loop
|
|
31
|
-
- **
|
|
32
|
-
- **Improved**: roll-brief 输出格式 — 序号命名、省略空 section、元信息格式精简,减少无效噪音
|
|
32
|
+
- **Added**: `roll loop monitor` — 一屏看 loop/dream/brief 三个调度服务状态
|
|
33
|
+
- **Fixed**: dashboard 待办数、release 状态显示问题
|
|
34
|
+
- **Fixed**: loop 异常退出后队列卡住不再继续执行
|
|
35
|
+
- **Improved**: 简报输出更精简,去掉空白段落和冗余信息
|
|
33
36
|
|
|
34
37
|
## v2026.511.1
|
|
35
|
-
- **Changed**:
|
|
36
|
-
- **Added**:
|
|
37
|
-
- **Fixed**: CI 测试环境兼容 — 移除依赖本地 state.yaml 的 hello_world.bats,修复 GitHub Actions 持续失败
|
|
38
|
+
- **Changed**: macOS 上 loop 调度切换到 launchd,比 crontab 更稳定
|
|
39
|
+
- **Added**: agent 跳过 TCR 节奏时自动拦回 Todo,强制重做
|
|
38
40
|
|
|
39
41
|
## v2026.510.10
|
|
40
42
|
- **Fixed**: release.sh changelog 同步时序修复 — 修正条件逻辑和执行顺序,确保每次发版时 changelog 正确更新
|
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.511.
|
|
7
|
+
VERSION="2026.511.8"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -531,6 +531,35 @@ _sync_skills() {
|
|
|
531
531
|
# COMMAND: setup [--force]
|
|
532
532
|
# Initialize ~/.roll/ and sync everything to AI tools in one step
|
|
533
533
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
534
|
+
# Ensures tmux is available (US-AUTO-026 promoted it from soft to required
|
|
535
|
+
# dependency for visible loop runs). On macOS attempts `brew install tmux`
|
|
536
|
+
# when brew exists; elsewhere prints the install command. Never fails the
|
|
537
|
+
# setup main flow — returns 0 even if install was not possible so the rest
|
|
538
|
+
# of `roll setup` proceeds.
|
|
539
|
+
_ensure_tmux() {
|
|
540
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
541
|
+
return 0
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
local os; os="$(uname)"
|
|
545
|
+
if [[ "$os" == "Darwin" ]]; then
|
|
546
|
+
if command -v brew >/dev/null 2>&1; then
|
|
547
|
+
info "tmux not found — installing via brew... 未安装 tmux,正在通过 brew 安装..."
|
|
548
|
+
if brew install tmux >/dev/null 2>&1; then
|
|
549
|
+
ok "tmux installed. tmux 已安装。"
|
|
550
|
+
return 0
|
|
551
|
+
fi
|
|
552
|
+
warn "brew install tmux failed — install manually: brew install tmux brew 安装失败,请手动 'brew install tmux'"
|
|
553
|
+
return 0
|
|
554
|
+
fi
|
|
555
|
+
warn "tmux required but brew not available — install manually: brew install tmux 缺少 brew,请手动 'brew install tmux'"
|
|
556
|
+
return 0
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
warn "tmux required — install via your package manager (e.g. apt install tmux / pacman -S tmux) 请用系统包管理器安装 tmux"
|
|
560
|
+
return 0
|
|
561
|
+
}
|
|
562
|
+
|
|
534
563
|
cmd_setup() {
|
|
535
564
|
local force=false
|
|
536
565
|
while [[ $# -gt 0 ]]; do
|
|
@@ -555,6 +584,10 @@ cmd_setup() {
|
|
|
555
584
|
_peer_ensure_state_dir
|
|
556
585
|
ok "Peer state directory ready. Peer 状态目录已就绪。"
|
|
557
586
|
|
|
587
|
+
echo ""
|
|
588
|
+
info "Ensuring tmux is installed (required for visible loop runs)... 正在确认 tmux 已安装..."
|
|
589
|
+
_ensure_tmux
|
|
590
|
+
|
|
558
591
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
559
592
|
echo ""
|
|
560
593
|
info "Installing launchd plists... 正在安装 LaunchAgents..."
|
|
@@ -1751,6 +1784,8 @@ _LOOP_TAG="# roll-loop"
|
|
|
1751
1784
|
_SHARED_ROOT="${HOME}/.shared/roll"
|
|
1752
1785
|
_LOOP_STATE="${HOME}/.shared/roll/loop/state.yaml"
|
|
1753
1786
|
_LOOP_ALERT="${HOME}/.shared/roll/loop/ALERT.md"
|
|
1787
|
+
_LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
1788
|
+
_LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
|
|
1754
1789
|
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
1755
1790
|
|
|
1756
1791
|
# Returns a filesystem-safe slug combining the project basename and a 6-char
|
|
@@ -1847,26 +1882,64 @@ _write_runner_script() {
|
|
|
1847
1882
|
|
|
1848
1883
|
# Like _write_runner_script but prepends an active window guard.
|
|
1849
1884
|
# Silently exits when current hour is outside [active_start, active_end).
|
|
1885
|
+
# When tmux is available, wraps the inner command in a detached tmux session
|
|
1886
|
+
# named `roll-loop-<slug>` so `roll loop attach` can watch in real time.
|
|
1887
|
+
# Falls back to headless execution when tmux is not installed.
|
|
1850
1888
|
_write_loop_runner_script() {
|
|
1851
1889
|
local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
|
|
1852
1890
|
local active_start="${5:-10}" active_end="${6:-18}"
|
|
1853
1891
|
mkdir -p "$(dirname "$script_path")"
|
|
1892
|
+
|
|
1893
|
+
local inner_path="${script_path%.sh}-inner.sh"
|
|
1894
|
+
# Inject --verbose into `claude -p` so the tmux-attach view shows live
|
|
1895
|
+
# tool-use and reasoning events (loop is launchd-triggered, the user is
|
|
1896
|
+
# not driving it — visibility is required, not optional).
|
|
1897
|
+
local cmd_verbose="${cmd/claude -p/claude --verbose -p}"
|
|
1898
|
+
cat > "$inner_path" << INNER
|
|
1899
|
+
#!/bin/bash -l
|
|
1900
|
+
export PATH="/opt/homebrew/bin:\$PATH"
|
|
1901
|
+
cd "${project_path}" && ${cmd_verbose}
|
|
1902
|
+
INNER
|
|
1903
|
+
chmod +x "$inner_path"
|
|
1904
|
+
|
|
1854
1905
|
cat > "$script_path" << SCRIPT
|
|
1855
1906
|
#!/bin/bash -l
|
|
1856
1907
|
h=\$(printf '%d' "\$(date +%H)")
|
|
1857
1908
|
if [ "\$h" -lt ${active_start} ] || [ "\$h" -ge ${active_end} ]; then exit 0; fi
|
|
1858
1909
|
LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1910
|
+
SESSION="roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1911
|
+
INNER_SCRIPT="${inner_path}"
|
|
1912
|
+
LOG="${log_path}"
|
|
1859
1913
|
if [ -f "\$LOCK" ]; then
|
|
1860
1914
|
prev_pid=\$(head -1 "\$LOCK" 2>/dev/null || echo "")
|
|
1861
1915
|
if [ -n "\$prev_pid" ] && kill -0 "\$prev_pid" 2>/dev/null; then
|
|
1862
|
-
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "
|
|
1916
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "\$LOG"
|
|
1863
1917
|
exit 0
|
|
1864
1918
|
fi
|
|
1865
1919
|
rm -f "\$LOCK"
|
|
1866
1920
|
fi
|
|
1867
1921
|
echo "\$\$" > "\$LOCK"
|
|
1868
1922
|
trap 'rm -f "\$LOCK"' EXIT
|
|
1869
|
-
|
|
1923
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
1924
|
+
tmux kill-session -t "\$SESSION" 2>/dev/null || true
|
|
1925
|
+
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
1926
|
+
tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
|
|
1927
|
+
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
1928
|
+
# tmux session so the user can watch the loop work in real time. Best-effort
|
|
1929
|
+
# focus retention: capture the current frontmost app and re-activate after.
|
|
1930
|
+
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ] && command -v osascript >/dev/null 2>&1; then
|
|
1931
|
+
(osascript \\
|
|
1932
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
1933
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
1934
|
+
-e 'delay 0.3' \\
|
|
1935
|
+
-e 'try' \\
|
|
1936
|
+
-e 'tell application _prev to activate' \\
|
|
1937
|
+
-e 'end try' >/dev/null 2>&1) &
|
|
1938
|
+
fi
|
|
1939
|
+
while tmux has-session -t "\$SESSION" 2>/dev/null; do sleep 5; done
|
|
1940
|
+
else
|
|
1941
|
+
bash "\$INNER_SCRIPT" >> "\$LOG" 2>&1
|
|
1942
|
+
fi
|
|
1870
1943
|
SCRIPT
|
|
1871
1944
|
chmod +x "$script_path"
|
|
1872
1945
|
}
|
|
@@ -1978,9 +2051,13 @@ cmd_loop() {
|
|
|
1978
2051
|
now) _loop_now ;;
|
|
1979
2052
|
status) _loop_status ;;
|
|
1980
2053
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
2054
|
+
runs) _loop_runs "$@" ;;
|
|
2055
|
+
attach) _loop_attach ;;
|
|
2056
|
+
mute) _loop_mute ;;
|
|
2057
|
+
unmute) _loop_unmute ;;
|
|
1981
2058
|
resume) _loop_resume ;;
|
|
1982
2059
|
reset) _loop_reset ;;
|
|
1983
|
-
*) err "Usage: roll loop <on|off|now|status|monitor|resume|reset>"; exit 1 ;;
|
|
2060
|
+
*) err "Usage: roll loop <on|off|now|status|monitor|runs|attach|mute|unmute|resume|reset>"; exit 1 ;;
|
|
1984
2061
|
esac
|
|
1985
2062
|
}
|
|
1986
2063
|
|
|
@@ -2118,6 +2195,12 @@ _loop_status() {
|
|
|
2118
2195
|
echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
|
|
2119
2196
|
fi
|
|
2120
2197
|
fi
|
|
2198
|
+
echo ""
|
|
2199
|
+
if [[ -f "$_LOOP_MUTE_FILE" ]]; then
|
|
2200
|
+
echo -e " Auto-attach ${YELLOW}muted${NC} run: roll loop unmute"
|
|
2201
|
+
else
|
|
2202
|
+
echo -e " Auto-attach ${GREEN}live${NC} run: roll loop mute"
|
|
2203
|
+
fi
|
|
2121
2204
|
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
|
|
2122
2205
|
[[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
|
|
2123
2206
|
echo ""
|
|
@@ -2143,6 +2226,138 @@ _loop_reset() {
|
|
|
2143
2226
|
fi
|
|
2144
2227
|
}
|
|
2145
2228
|
|
|
2229
|
+
# Suppress the auto-attach popup. When the marker file exists, runner scripts
|
|
2230
|
+
# skip the osascript Terminal-popup step on next fire. Loop output still goes
|
|
2231
|
+
# to tmux + log; users can run `roll loop attach` manually.
|
|
2232
|
+
_loop_mute() {
|
|
2233
|
+
mkdir -p "$(dirname "$_LOOP_MUTE_FILE")"
|
|
2234
|
+
: > "$_LOOP_MUTE_FILE"
|
|
2235
|
+
ok "🔇 muted — auto-attach disabled 已静音,自动弹窗已关闭"
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
# Re-enable the auto-attach popup.
|
|
2239
|
+
_loop_unmute() {
|
|
2240
|
+
rm -f "$_LOOP_MUTE_FILE"
|
|
2241
|
+
ok "🔔 unmuted — auto-attach live 已恢复,自动弹窗已开启"
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
# Attach to the tmux session a running loop iteration writes to. Returns 1 when
|
|
2245
|
+
# tmux is missing or no session exists for the current project.
|
|
2246
|
+
_loop_attach() {
|
|
2247
|
+
local project_path; project_path=$(pwd -P)
|
|
2248
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2249
|
+
local session="roll-loop-${slug}"
|
|
2250
|
+
|
|
2251
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
2252
|
+
warn "tmux not installed — install with 'brew install tmux' 请先安装 tmux"
|
|
2253
|
+
return 1
|
|
2254
|
+
fi
|
|
2255
|
+
|
|
2256
|
+
if ! tmux has-session -t "$session" 2>/dev/null; then
|
|
2257
|
+
info "No running loop session for this project 当前项目无运行中的 loop"
|
|
2258
|
+
info "Wait for next scheduled fire, or run: roll loop now"
|
|
2259
|
+
return 1
|
|
2260
|
+
fi
|
|
2261
|
+
|
|
2262
|
+
exec tmux attach -t "$session"
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
# Pretty-print a duration in seconds as "Xs" / "Ym" / "Yh Zm".
|
|
2266
|
+
_loop_runs_dur() {
|
|
2267
|
+
local s="${1:-0}"
|
|
2268
|
+
if [[ "$s" -lt 60 ]]; then printf "%ds" "$s"
|
|
2269
|
+
elif [[ "$s" -lt 3600 ]]; then printf "%dm" "$((s / 60))"
|
|
2270
|
+
else printf "%dh %dm" "$((s / 3600))" "$(((s % 3600) / 60))"
|
|
2271
|
+
fi
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
# Format a single JSONL run record for display.
|
|
2275
|
+
_loop_runs_format_line() {
|
|
2276
|
+
local line="$1" show_project="$2"
|
|
2277
|
+
command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
|
|
2278
|
+
|
|
2279
|
+
local ts status project tcr duration alerts run_id reason built_count built_csv skipped_count
|
|
2280
|
+
ts=$(jq -r '.ts // ""' <<<"$line")
|
|
2281
|
+
status=$(jq -r '.status // ""' <<<"$line")
|
|
2282
|
+
project=$(jq -r '.project // ""' <<<"$line")
|
|
2283
|
+
tcr=$(jq -r '.tcr_count // 0' <<<"$line")
|
|
2284
|
+
duration=$(jq -r '.duration_sec // 0' <<<"$line")
|
|
2285
|
+
alerts=$(jq -r '.alerts // 0' <<<"$line")
|
|
2286
|
+
run_id=$(jq -r '.run_id // ""' <<<"$line")
|
|
2287
|
+
reason=$(jq -r '.reason // ""' <<<"$line")
|
|
2288
|
+
built_count=$(jq -r '(.built // []) | length' <<<"$line")
|
|
2289
|
+
built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
|
|
2290
|
+
skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
|
|
2291
|
+
|
|
2292
|
+
local hhmm; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
|
|
2293
|
+
local prefix=""
|
|
2294
|
+
if [[ "$show_project" == "true" ]]; then
|
|
2295
|
+
prefix="[$(basename "$project")] "
|
|
2296
|
+
fi
|
|
2297
|
+
|
|
2298
|
+
case "$status" in
|
|
2299
|
+
built)
|
|
2300
|
+
local skipped_note=""
|
|
2301
|
+
[[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
|
|
2302
|
+
printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
|
|
2303
|
+
"$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
|
|
2304
|
+
;;
|
|
2305
|
+
idle)
|
|
2306
|
+
printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
|
|
2307
|
+
;;
|
|
2308
|
+
failed)
|
|
2309
|
+
local msg="${reason:-unknown}"
|
|
2310
|
+
printf " %s %s✗ FAILED — %s\n" "$hhmm" "$prefix" "$msg"
|
|
2311
|
+
;;
|
|
2312
|
+
*)
|
|
2313
|
+
printf " %s %s? %s\n" "$hhmm" "$prefix" "$status"
|
|
2314
|
+
;;
|
|
2315
|
+
esac
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
# `roll loop runs [N] [--all]` — show recent loop iteration summaries.
|
|
2319
|
+
_loop_runs() {
|
|
2320
|
+
local n=10 all_flag=false
|
|
2321
|
+
while [[ $# -gt 0 ]]; do
|
|
2322
|
+
case "$1" in
|
|
2323
|
+
--all) all_flag=true; shift ;;
|
|
2324
|
+
[0-9]*) n="$1"; shift ;;
|
|
2325
|
+
*) shift ;;
|
|
2326
|
+
esac
|
|
2327
|
+
done
|
|
2328
|
+
|
|
2329
|
+
if [[ ! -f "$_LOOP_RUNS" ]] || [[ ! -s "$_LOOP_RUNS" ]]; then
|
|
2330
|
+
echo "No loop runs yet 尚无 loop 运行记录"
|
|
2331
|
+
return 0
|
|
2332
|
+
fi
|
|
2333
|
+
|
|
2334
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
2335
|
+
err "jq required for 'roll loop runs' 需要 jq"
|
|
2336
|
+
return 1
|
|
2337
|
+
fi
|
|
2338
|
+
|
|
2339
|
+
local project_path; project_path=$(pwd -P)
|
|
2340
|
+
local filtered
|
|
2341
|
+
if $all_flag; then
|
|
2342
|
+
filtered=$(cat "$_LOOP_RUNS")
|
|
2343
|
+
else
|
|
2344
|
+
filtered=$(jq -c --arg p "$project_path" 'select(.project == $p)' "$_LOOP_RUNS")
|
|
2345
|
+
fi
|
|
2346
|
+
|
|
2347
|
+
if [[ -z "$filtered" ]]; then
|
|
2348
|
+
echo "No loop runs for current project 当前项目尚无 loop 运行记录"
|
|
2349
|
+
return 0
|
|
2350
|
+
fi
|
|
2351
|
+
|
|
2352
|
+
local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
|
|
2353
|
+
local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
|
|
2354
|
+
|
|
2355
|
+
while IFS= read -r line; do
|
|
2356
|
+
[[ -z "$line" ]] && continue
|
|
2357
|
+
_loop_runs_format_line "$line" "$all_flag"
|
|
2358
|
+
done <<<"$recent"
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2146
2361
|
# Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
|
|
2147
2362
|
_loop_tcr_count() {
|
|
2148
2363
|
local started_at="$1"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.511.
|
|
3
|
+
"version": "2026.511.8",
|
|
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"
|
|
@@ -45,83 +45,112 @@ Create mode:
|
|
|
45
45
|
|
|
46
46
|
### 3. Filter for External Content
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
- Progress tables, completion percentages
|
|
50
|
-
- "As a / I can / So that" format
|
|
51
|
-
- Detailed AC checklists
|
|
52
|
-
- Technical debt, internal file paths
|
|
53
|
-
- Test case counts, architecture diagrams
|
|
48
|
+
CHANGELOG 是给**使用者**看的,不是给维护者看的。一句话讲清"用户能做什么 / 不再被什么坑",能不写就不写。
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
50
|
+
**完全跳过(不写入 CHANGELOG):**
|
|
51
|
+
- 测试基建(teardown 清理、test isolation、bats helper、CI 时序)
|
|
52
|
+
- prompt / SKILL.md 内部契约(schema 锁定、enum 强制、contract test)
|
|
53
|
+
- 内部重构(提取函数、变量改名、目录调整)
|
|
54
|
+
- 只有开发者会遇到的 bug(release.sh 自身逻辑、TCR 节奏调整)
|
|
55
|
+
- 任何"用户体验不变"的改动
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
判断准则:**如果用户读了这条记录,他不会改变使用方式,就别写。**
|
|
62
58
|
|
|
63
|
-
|
|
59
|
+
**保留:**
|
|
60
|
+
- 用户能直接调用的新功能 / 新命令
|
|
61
|
+
- 用户实际遇到过的 bug 修复
|
|
62
|
+
- 看得见的体验变化(布局、文案、速度、可见性)
|
|
63
|
+
- 影响安装、升级、配置的改动
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
**写法约束:**
|
|
66
|
+
|
|
67
|
+
1. **一行,30 字以内**。超了就是太啰嗦。
|
|
68
|
+
2. **不写实现细节**:禁止文件路径、函数名、字段列表、命令参数、配置键名。
|
|
69
|
+
3. **不写数字细节**:"3 个服务"、"60+ ghost" 这种内部状态不写。
|
|
70
|
+
4. **说人话**:避免 "幂等"、"trap"、"epoch" 等技术黑话;说"做两次效果一样"、"异常退出也会清理"、"启动时间"。
|
|
71
|
+
5. **句式**:`功能名 — 用户能做什么 / 解决了什么麻烦`。
|
|
72
|
+
|
|
73
|
+
**语言:中文。**
|
|
74
|
+
|
|
75
|
+
具体对比(都是真实的 roll 改动):
|
|
76
|
+
|
|
77
|
+
❌ 全是实现细节:
|
|
66
78
|
```
|
|
67
|
-
- **Added**: roll
|
|
68
|
-
- **Fixed**: 同步时清理已删除文件,防止用户机器残留幽灵文件
|
|
79
|
+
- **Added**: `roll loop runs` 每次 loop 运行的快速可见性 — 单次 loop 结束追加一行 JSON 到 `~/.shared/roll/loop/runs.jsonl`(含 ts/project/run_id/status/built/skipped/alerts/tcr_count/duration_sec),新命令 `roll loop runs [N] [--all]` 倒序显示最近 N 次(默认 10)。
|
|
69
80
|
```
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
✅ 讲价值:
|
|
72
83
|
```
|
|
73
|
-
- **Added**:
|
|
74
|
-
- **Fixed**: Sync prunes stale files to prevent ghost files
|
|
84
|
+
- **Added**: `roll loop runs` — 随时查看 loop 最近几次都跑了什么
|
|
75
85
|
```
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
❌ 内部信息扎堆:
|
|
88
|
+
```
|
|
89
|
+
- **Fixed**: 集成测试 launchd ghost 泄漏 — `integration_teardown` 在删除 TEST_TMP 之前,先 `launchctl bootout` 该沙箱里被 `roll loop on` 注册到 user gui domain 的所有 `com.roll.*` 服务。
|
|
90
|
+
```
|
|
78
91
|
|
|
79
|
-
|
|
92
|
+
✅ **直接跳过**:测试基建修复,用户感知不到。
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
❌ 参数列表+黑话:
|
|
95
|
+
```
|
|
96
|
+
- **Added**: Loop 并发安全 — runner script 启动时写入 per-project LOCK 文件并检测重入;活跃 PID 已存在则跳过本次,残留死 LOCK 自动清理;正常/异常退出均通过 trap 清掉 LOCK。
|
|
84
97
|
```
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
✅ 用户视角:
|
|
100
|
+
```
|
|
101
|
+
- **Fixed**: 多个 loop 实例不会再互相打架(重复触发自动跳过)
|
|
102
|
+
```
|
|
87
103
|
|
|
88
|
-
|
|
104
|
+
### 4. Section Header — Always `## Unreleased`
|
|
105
|
+
|
|
106
|
+
**⚠️ do NOT guess version numbers.** Only `scripts/release.sh` assigns concrete
|
|
107
|
+
versions, and it only does so at the moment of a real release. Until then,
|
|
108
|
+
every new bullet goes under `## Unreleased` at the top of CHANGELOG.md.
|
|
89
109
|
|
|
90
110
|
```
|
|
91
|
-
##
|
|
111
|
+
## Unreleased
|
|
112
|
+
- **Added**: ...new entries here...
|
|
113
|
+
- **Fixed**: ...
|
|
92
114
|
```
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
When `release.sh` runs, it renames `## Unreleased` to `## v{N}` (where N is
|
|
117
|
+
computed from git tags) — that's the single moment a version label gets
|
|
118
|
+
assigned.
|
|
119
|
+
|
|
120
|
+
Do NOT read `package.json` version, do NOT call `git describe`, do NOT invent
|
|
121
|
+
version numbers like `v2026.511.8`. Just write to `## Unreleased`.
|
|
95
122
|
|
|
96
123
|
### 5. Generate CHANGELOG.md
|
|
97
124
|
|
|
98
|
-
**Create mode** (first time):
|
|
125
|
+
**Create mode** (first time, no CHANGELOG.md yet):
|
|
99
126
|
```markdown
|
|
100
127
|
# Changelog
|
|
101
128
|
|
|
129
|
+
## Unreleased
|
|
130
|
+
- **Added**: ...current deploy's entries...
|
|
131
|
+
|
|
102
132
|
## 2026.05.10
|
|
103
|
-
- **Added**:
|
|
104
|
-
|
|
133
|
+
- **Added**: ...historical entries from completed Stories before today...
|
|
134
|
+
```
|
|
105
135
|
|
|
106
|
-
|
|
107
|
-
- **Added**: BB 注入模式 — 对未集成 Black Box 的页面自动注入诊断探针,统一数据采集接口
|
|
136
|
+
**Append mode** (most common — CHANGELOG.md exists):
|
|
108
137
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
1. Find `## Unreleased` heading at the top of CHANGELOG.md.
|
|
139
|
+
2. If it exists → append new bullets under it (do NOT create a new section).
|
|
140
|
+
3. If it doesn't exist → insert a fresh `## Unreleased` at the very top (right after the `# Changelog` title) with the new bullets.
|
|
112
141
|
|
|
113
|
-
**Append mode** (subsequent):
|
|
114
142
|
```markdown
|
|
115
143
|
# Changelog
|
|
116
144
|
|
|
117
|
-
##
|
|
118
|
-
- **Added**:
|
|
145
|
+
## Unreleased
|
|
146
|
+
- **Added**: ...just-deployed entry appended here...
|
|
147
|
+
- **Fixed**: ...another just-deployed entry...
|
|
119
148
|
|
|
120
|
-
##
|
|
149
|
+
## v2026.05.07 ← previous releases left untouched
|
|
121
150
|
- ...
|
|
122
151
|
```
|
|
123
152
|
|
|
124
|
-
**Ordering**:
|
|
153
|
+
**Ordering**: Unreleased always at top. Below it, released versions in reverse chronological order.
|
|
125
154
|
|
|
126
155
|
### 6. Commit Update
|
|
127
156
|
|
|
@@ -113,29 +113,31 @@ complexity or prevent future bugs. Ignore cosmetic issues.
|
|
|
113
113
|
|
|
114
114
|
### Dream Log (docs/dream/YYYY-MM-DD.md)
|
|
115
115
|
|
|
116
|
-
Always write a log, even when no REFACTOR entries are created
|
|
116
|
+
Always write a log, even when no REFACTOR entries are created. Output uses
|
|
117
|
+
Chinese to align with roll-brief style — easier for the morning reader to scan
|
|
118
|
+
without context switching:
|
|
117
119
|
|
|
118
120
|
```markdown
|
|
119
121
|
# Dream Log {YYYY-MM-DD}
|
|
120
122
|
|
|
121
|
-
##
|
|
122
|
-
-
|
|
123
|
-
-
|
|
123
|
+
## 概要
|
|
124
|
+
- 扫描项:死代码 / 架构漂移 / 裁剪候选 / 新兴模式
|
|
125
|
+
- 发现:{N} 项标记,{M} 个 REFACTOR 条目已创建
|
|
124
126
|
|
|
125
|
-
##
|
|
126
|
-
{
|
|
127
|
+
## 死代码
|
|
128
|
+
{发现内容 或 "未发现死代码。"}
|
|
127
129
|
|
|
128
|
-
##
|
|
129
|
-
{
|
|
130
|
+
## 架构漂移
|
|
131
|
+
{发现内容 或 "未发现架构漂移。"}
|
|
130
132
|
|
|
131
|
-
##
|
|
132
|
-
{
|
|
133
|
+
## 裁剪候选
|
|
134
|
+
{发现内容 或 "未发现可裁剪项。"}
|
|
133
135
|
|
|
134
|
-
##
|
|
135
|
-
{
|
|
136
|
+
## 新兴模式
|
|
137
|
+
{发现内容 或 "未发现可提取的重复模式。"}
|
|
136
138
|
|
|
137
|
-
## REFACTOR
|
|
138
|
-
{
|
|
139
|
+
## 创建的 REFACTOR 条目
|
|
140
|
+
{列表 或 "无。"}
|
|
139
141
|
```
|
|
140
142
|
|
|
141
143
|
## Scheduler Configuration
|
|
@@ -151,7 +153,7 @@ The cron entry is generated using the configured agent — no manual cron editin
|
|
|
151
153
|
|
|
152
154
|
If the scan fails partway through:
|
|
153
155
|
|
|
154
|
-
1. Write partial results to `docs/dream/YYYY-MM-DD.md` with a `##
|
|
156
|
+
1. Write partial results to `docs/dream/YYYY-MM-DD.md` with a `## 状态:部分完成` header
|
|
155
157
|
2. Do not write incomplete REFACTOR entries to BACKLOG
|
|
156
158
|
3. Log the error to `~/.shared/roll/dream/error.log`
|
|
157
159
|
|
|
@@ -153,6 +153,67 @@ last_run_items: [US-AUTH-003, FIX-007]
|
|
|
153
153
|
last_run_outcome: success
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
Then append a JSONL record to `~/.shared/roll/loop/runs.jsonl` for per-iteration
|
|
157
|
+
visibility (one line per cycle, append-only — never delete or rewrite earlier lines).
|
|
158
|
+
|
|
159
|
+
**⚠️ Strict schema contract — do NOT deviate.** Every field has exactly one
|
|
160
|
+
canonical form. Synonyms like `"success"`, `"noop"`, `"completed"` are forbidden
|
|
161
|
+
for `status`. Numbers and arrays cannot be interchanged. UTC `Z` suffix only,
|
|
162
|
+
no timezone offsets. **No extra fields** — emit only the keys listed below (plus
|
|
163
|
+
optional `reason` when `status="failed"`); do not add `note`, `comment`,
|
|
164
|
+
`details`, `info`, etc. If you feel the urge to annotate, put it in the cycle's
|
|
165
|
+
final report in `cron.log` instead.
|
|
166
|
+
|
|
167
|
+
**Canonical record (copy this exact shape, fill in real values):**
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{"ts":"2026-05-11T11:46:43Z","project":"roll-d9dfa0","run_id":"loop-20260511-1911","status":"built","built":["US-AUTO-024","US-AUTO-025"],"skipped":[],"alerts":[],"tcr_count":5,"duration_sec":2080}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Field contract — types are enforced**:
|
|
174
|
+
|
|
175
|
+
| Field | Type | Format / Enum |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `ts` | string | ISO 8601 **UTC** with `Z` suffix. Get via `date -u +%Y-%m-%dT%H:%M:%SZ`. Never use `+08:00` or other offsets. |
|
|
178
|
+
| `project` | string | Project **slug** only (e.g. `roll-d9dfa0`), NOT the absolute path. Derive from `basename` of plist label or `_project_slug` output. |
|
|
179
|
+
| `run_id` | string | Matches `state.yaml` `run_id` exactly. Format: `loop-YYYYMMDD-HHMM`. |
|
|
180
|
+
| `status` | enum | Exactly one of: `built` (≥1 story shipped), `idle` (no Todo items found), `failed` (paused/error). **No synonyms.** |
|
|
181
|
+
| `built` | array<string> | Story ids completed this cycle. `[]` when none. **Always array, never null/number.** |
|
|
182
|
+
| `skipped` | array<string> | Story ids skipped because they were `🔨 In Progress`. `[]` when none. **Always array.** |
|
|
183
|
+
| `alerts` | array<string> | Newly raised ALERT identifiers/tags this cycle. `[]` when none. **Always array, never number.** |
|
|
184
|
+
| `tcr_count` | integer | Total `tcr:` prefix commits made this cycle. `0` when none. |
|
|
185
|
+
| `duration_sec` | integer | Seconds from cycle start to completion. Integer only, no decimals. |
|
|
186
|
+
|
|
187
|
+
Optional field, only when `status == "failed"`:
|
|
188
|
+
- `reason` (string): short human-readable explanation.
|
|
189
|
+
|
|
190
|
+
**Write recipe:**
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
194
|
+
project=$(basename "$(pwd)" | sed 's/.*-//') # or use _project_slug if available
|
|
195
|
+
# duration_sec = cycle_end_epoch - cycle_start_epoch (track at Step 1)
|
|
196
|
+
# tcr_count = git log --oneline --since="<cycle_start>" | grep -c '^[a-f0-9]* tcr:'
|
|
197
|
+
|
|
198
|
+
jq -nc \
|
|
199
|
+
--arg ts "$ts" \
|
|
200
|
+
--arg project "$project" \
|
|
201
|
+
--arg run_id "$run_id" \
|
|
202
|
+
--arg status "built" \
|
|
203
|
+
--argjson built '["US-AUTO-024"]' \
|
|
204
|
+
--argjson skipped '[]' \
|
|
205
|
+
--argjson alerts '[]' \
|
|
206
|
+
--argjson tcr_count 14 \
|
|
207
|
+
--argjson duration_sec 1680 \
|
|
208
|
+
'{ts:$ts, project:$project, run_id:$run_id, status:$status,
|
|
209
|
+
built:$built, skipped:$skipped, alerts:$alerts,
|
|
210
|
+
tcr_count:$tcr_count, duration_sec:$duration_sec}' \
|
|
211
|
+
>> ~/.shared/roll/loop/runs.jsonl
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The companion read-side is `roll loop runs [N] [--all]` — shows the most recent
|
|
215
|
+
N records (default 10) for the current project, or across all projects with `--all`.
|
|
216
|
+
|
|
156
217
|
## Failure Handling
|
|
157
218
|
|
|
158
219
|
### Network Error (transient)
|
|
@@ -235,6 +296,38 @@ roll loop status # show current state
|
|
|
235
296
|
roll loop now # execute one cycle immediately
|
|
236
297
|
```
|
|
237
298
|
|
|
299
|
+
### Live attach (transparency)
|
|
300
|
+
|
|
301
|
+
Each loop iteration runs inside a detached tmux session named
|
|
302
|
+
`roll-loop-<slug>` (tmux is a required dependency — `roll setup` auto-installs
|
|
303
|
+
it via Homebrew on macOS, or prints the install command elsewhere).
|
|
304
|
+
|
|
305
|
+
**Default — auto-attach popup**: when the loop fires, a background Terminal
|
|
306
|
+
window pops up running `tmux attach -t roll-loop-<slug>`. You can watch the
|
|
307
|
+
agent work in real time without typing anything. The popup is best-effort
|
|
308
|
+
focus-retaining (it captures the previously-active app and restores focus
|
|
309
|
+
after the window appears) and the tmux session keeps running even if you
|
|
310
|
+
close the window.
|
|
311
|
+
|
|
312
|
+
**Manual attach** (any time):
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
roll loop attach # exec tmux attach -t roll-loop-<slug>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Press `Ctrl-B D` to detach — the loop continues running uninterrupted.
|
|
319
|
+
|
|
320
|
+
**Mute / unmute the popup**:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
roll loop mute # 🔇 — suppress auto-attach popup (loop still runs in tmux)
|
|
324
|
+
roll loop unmute # 🔔 — re-enable the popup
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Mute state is a single marker file at `~/.shared/roll/mute` and is shared
|
|
328
|
+
across all projects on this machine. Check the current state with
|
|
329
|
+
`roll loop status` — it shows an `Auto-attach: live | muted` line.
|
|
330
|
+
|
|
238
331
|
## Integration Map
|
|
239
332
|
|
|
240
333
|
```
|