@seanyao/roll 2026.511.5 → 2026.511.7
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 +25 -0
- package/bin/roll +247 -10
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +17 -15
- package/skills/roll-build/SKILL.md +1 -1
- package/skills/roll-fix/SKILL.md +1 -1
- package/skills/roll-loop/SKILL.md +136 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.511.11
|
|
4
|
+
- **Added**: loop 默认 auto-attach 弹窗 — 每次 loop 触发时,runner script 自动通过 osascript 开一个背景 Terminal 窗口 `tmux attach -t roll-loop-<slug>`,看 claude 实时打字干活;弹窗用 `delay 0.3` + 还原前一个 frontmost app 做到不抢焦点,tmux session 结束窗口保留供回看,关掉窗口 loop 仍在 tmux 里继续跑。
|
|
5
|
+
- **Added**: `roll loop mute` / `roll loop unmute` 一键开关 — 不想看弹窗时 `roll loop mute` 创建 `~/.shared/roll/mute` 标记文件即刻静音,`unmute` 删掉它恢复;mute 状态对所有项目生效(一个开关治整机),`roll loop status` 新增 `Auto-attach live | muted` 一行实时显示。
|
|
6
|
+
- **Added**: tmux 升级为 `roll setup` 必装依赖 — 新增 `_ensure_tmux` helper:macOS 自动 `brew install tmux`(无 brew 给手动命令),Linux/其他系统打印对应包管理器安装指引;任何失败路径都返回 0,不阻塞 setup 主流程。
|
|
7
|
+
- **Fixed**: launchd runner 缺 brew PATH 导致 hook 子进程报 `node: command not found` — launchd 默认 PATH 不含 `/opt/homebrew/bin`,claude 通过 `sh -c` 调 SessionEnd hook 时找不到 node;inner runner 模板显式 `export PATH="/opt/homebrew/bin:$PATH"` 让整条 fork 链都能拿到 brew 工具。
|
|
8
|
+
- **Fixed**: runs.jsonl schema 漂移 — 早期 claude 在 status/ts/alerts/project 字段自由发挥(`built` vs `success` vs `noop`、UTC vs `+08:00`、number vs array、全路径 vs slug)。SKILL Step 5 改为"严格契约":ts 强制 UTC Z 后缀、project 用 slug、alerts/built/skipped 永远是数组、status 限定 `built/idle/failed` 三个 enum 无同义词;contract test 锁死关键不变量留在 prompt 里。
|
|
9
|
+
|
|
10
|
+
## v2026.511.9
|
|
11
|
+
- **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),不必等次日早报就能查到中间 13 次 loop 各干了啥。
|
|
12
|
+
- **Added**: loop 跑在 tmux session + `roll loop attach` 实时观看 — runner script 自动把 claude 包进 detached tmux session `roll-loop-<slug>`,输出同时 pipe 到 `cron.log`;执行 `roll loop attach` 可随时 attach 上去看它打字、写文件、commit,Ctrl-B D 分离后 loop 继续跑;未装 tmux 时自动 fallback 到原 headless 模式,零依赖回退。
|
|
13
|
+
- **Improved**: roll-.dream 日志改为中文输出 — Dream Log 输出模板(概要 / 死代码 / 架构漂移 / 裁剪候选 / 新兴模式 / 创建的 REFACTOR 条目)和"未发现 / 部分完成"等固定文案全部中文化,与 roll-brief 风格对齐,晨间扫一眼不再需要在中英文之间切换语境。
|
|
14
|
+
|
|
15
|
+
## v2026.511.8
|
|
16
|
+
- **Fixed**: 集成测试 launchd ghost 泄漏 — `integration_teardown` 在删除 TEST_TMP 之前,先 `launchctl bootout` 该沙箱里被 `roll loop on` 注册到 user gui domain 的所有 `com.roll.*` 服务,避免删 plist 后 launchd 仍保留指向不存在路径的 ghost 注册。
|
|
17
|
+
|
|
18
|
+
## v2026.511.7
|
|
19
|
+
- **Added**: loop 执行 story 前显式标记 🔨 In Progress — roll-loop SKILL 在调用 executor 之前先把 BACKLOG 中的故事状态从 📋 Todo 改为 🔨 In Progress 并提交 `chore: mark US-XXX in progress`,brief 简报和 peer agent 都能即时感知正在进行的工作,tcr 微提交不再"对 brief 不可见"。
|
|
20
|
+
- **Added**: loop 启动时孤儿 🔨 自愈 — 扫描 BACKLOG 中无对应 state.yaml running item 的 🔨 条目,视为上次崩溃残留,自动 revert 回 📋 Todo 并写 ALERT,避免被"卡"在错误的中间状态里。
|
|
21
|
+
- **Improved**: roll-build / roll-fix SKILL 状态转换段更新 — 显式接受 📋 Todo 或 🔨 In Progress 作为 ✅ Done 前置状态,loop 触发链路状态过渡更稳健。
|
|
22
|
+
|
|
23
|
+
## v2026.511.6
|
|
24
|
+
- **Added**: Loop 并发安全 — runner script 启动时写入 per-project LOCK 文件并检测重入;活跃 PID 已存在则跳过本次,残留死 LOCK 自动清理;正常/异常退出均通过 trap 清掉 LOCK。彻底防止两个 loop 实例同时启动造成的 BACKLOG/git 冲突。
|
|
25
|
+
- **Added**: roll-loop SKILL 显式声明 skip-🔨 In Progress 语义 — claude 扫 BACKLOG 时跳过已被人工或 peer agent 标记的执行中条目,为人机协同和多 agent 协作奠定基础。
|
|
26
|
+
- **Fixed**: 5 个 pre-existing 测试失败 — `run_roll` helper 切换到 TEST_TMP 作为 cwd 避免 slug 冲突;loop status 测试匹配三态显示新文案;dashboard 测试匹配 `_launchd_svc_state` + array 派生 schedule 的新结构。
|
|
27
|
+
|
|
3
28
|
## v2026.511.5
|
|
4
29
|
- **Fixed**: launchd plist 自动 reload — plist 内容变更且服务已加载时自动 unload + reload,升级 roll 后 loop 服务立即生效,无需手动重启
|
|
5
30
|
- **Improved**: roll loop status/monitor 三态展示 — 区分 ● 运行中 / ⚠ 已安装未加载 / ○ 未安装,并给出对应的自愈操作提示
|
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.7"
|
|
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,15 +1882,60 @@ _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
|
+
cat > "$inner_path" << INNER
|
|
1895
|
+
#!/bin/bash -l
|
|
1896
|
+
export PATH="/opt/homebrew/bin:\$PATH"
|
|
1897
|
+
cd "${project_path}" && ${cmd}
|
|
1898
|
+
INNER
|
|
1899
|
+
chmod +x "$inner_path"
|
|
1900
|
+
|
|
1854
1901
|
cat > "$script_path" << SCRIPT
|
|
1855
1902
|
#!/bin/bash -l
|
|
1856
1903
|
h=\$(printf '%d' "\$(date +%H)")
|
|
1857
1904
|
if [ "\$h" -lt ${active_start} ] || [ "\$h" -ge ${active_end} ]; then exit 0; fi
|
|
1858
|
-
|
|
1905
|
+
LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1906
|
+
SESSION="roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1907
|
+
INNER_SCRIPT="${inner_path}"
|
|
1908
|
+
LOG="${log_path}"
|
|
1909
|
+
if [ -f "\$LOCK" ]; then
|
|
1910
|
+
prev_pid=\$(head -1 "\$LOCK" 2>/dev/null || echo "")
|
|
1911
|
+
if [ -n "\$prev_pid" ] && kill -0 "\$prev_pid" 2>/dev/null; then
|
|
1912
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "\$LOG"
|
|
1913
|
+
exit 0
|
|
1914
|
+
fi
|
|
1915
|
+
rm -f "\$LOCK"
|
|
1916
|
+
fi
|
|
1917
|
+
echo "\$\$" > "\$LOCK"
|
|
1918
|
+
trap 'rm -f "\$LOCK"' EXIT
|
|
1919
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
1920
|
+
tmux kill-session -t "\$SESSION" 2>/dev/null || true
|
|
1921
|
+
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
1922
|
+
tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
|
|
1923
|
+
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
1924
|
+
# tmux session so the user can watch the loop work in real time. Best-effort
|
|
1925
|
+
# focus retention: capture the current frontmost app and re-activate after.
|
|
1926
|
+
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ] && command -v osascript >/dev/null 2>&1; then
|
|
1927
|
+
(osascript \\
|
|
1928
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
1929
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
1930
|
+
-e 'delay 0.3' \\
|
|
1931
|
+
-e 'try' \\
|
|
1932
|
+
-e 'tell application _prev to activate' \\
|
|
1933
|
+
-e 'end try' >/dev/null 2>&1) &
|
|
1934
|
+
fi
|
|
1935
|
+
while tmux has-session -t "\$SESSION" 2>/dev/null; do sleep 5; done
|
|
1936
|
+
else
|
|
1937
|
+
bash "\$INNER_SCRIPT" >> "\$LOG" 2>&1
|
|
1938
|
+
fi
|
|
1859
1939
|
SCRIPT
|
|
1860
1940
|
chmod +x "$script_path"
|
|
1861
1941
|
}
|
|
@@ -1967,9 +2047,13 @@ cmd_loop() {
|
|
|
1967
2047
|
now) _loop_now ;;
|
|
1968
2048
|
status) _loop_status ;;
|
|
1969
2049
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
2050
|
+
runs) _loop_runs "$@" ;;
|
|
2051
|
+
attach) _loop_attach ;;
|
|
2052
|
+
mute) _loop_mute ;;
|
|
2053
|
+
unmute) _loop_unmute ;;
|
|
1970
2054
|
resume) _loop_resume ;;
|
|
1971
2055
|
reset) _loop_reset ;;
|
|
1972
|
-
*) err "Usage: roll loop <on|off|now|status|monitor|resume|reset>"; exit 1 ;;
|
|
2056
|
+
*) err "Usage: roll loop <on|off|now|status|monitor|runs|attach|mute|unmute|resume|reset>"; exit 1 ;;
|
|
1973
2057
|
esac
|
|
1974
2058
|
}
|
|
1975
2059
|
|
|
@@ -2107,6 +2191,12 @@ _loop_status() {
|
|
|
2107
2191
|
echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
|
|
2108
2192
|
fi
|
|
2109
2193
|
fi
|
|
2194
|
+
echo ""
|
|
2195
|
+
if [[ -f "$_LOOP_MUTE_FILE" ]]; then
|
|
2196
|
+
echo -e " Auto-attach ${YELLOW}muted${NC} run: roll loop unmute"
|
|
2197
|
+
else
|
|
2198
|
+
echo -e " Auto-attach ${GREEN}live${NC} run: roll loop mute"
|
|
2199
|
+
fi
|
|
2110
2200
|
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
|
|
2111
2201
|
[[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
|
|
2112
2202
|
echo ""
|
|
@@ -2132,6 +2222,138 @@ _loop_reset() {
|
|
|
2132
2222
|
fi
|
|
2133
2223
|
}
|
|
2134
2224
|
|
|
2225
|
+
# Suppress the auto-attach popup. When the marker file exists, runner scripts
|
|
2226
|
+
# skip the osascript Terminal-popup step on next fire. Loop output still goes
|
|
2227
|
+
# to tmux + log; users can run `roll loop attach` manually.
|
|
2228
|
+
_loop_mute() {
|
|
2229
|
+
mkdir -p "$(dirname "$_LOOP_MUTE_FILE")"
|
|
2230
|
+
: > "$_LOOP_MUTE_FILE"
|
|
2231
|
+
ok "🔇 muted — auto-attach disabled 已静音,自动弹窗已关闭"
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
# Re-enable the auto-attach popup.
|
|
2235
|
+
_loop_unmute() {
|
|
2236
|
+
rm -f "$_LOOP_MUTE_FILE"
|
|
2237
|
+
ok "🔔 unmuted — auto-attach live 已恢复,自动弹窗已开启"
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
# Attach to the tmux session a running loop iteration writes to. Returns 1 when
|
|
2241
|
+
# tmux is missing or no session exists for the current project.
|
|
2242
|
+
_loop_attach() {
|
|
2243
|
+
local project_path; project_path=$(pwd -P)
|
|
2244
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2245
|
+
local session="roll-loop-${slug}"
|
|
2246
|
+
|
|
2247
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
2248
|
+
warn "tmux not installed — install with 'brew install tmux' 请先安装 tmux"
|
|
2249
|
+
return 1
|
|
2250
|
+
fi
|
|
2251
|
+
|
|
2252
|
+
if ! tmux has-session -t "$session" 2>/dev/null; then
|
|
2253
|
+
info "No running loop session for this project 当前项目无运行中的 loop"
|
|
2254
|
+
info "Wait for next scheduled fire, or run: roll loop now"
|
|
2255
|
+
return 1
|
|
2256
|
+
fi
|
|
2257
|
+
|
|
2258
|
+
exec tmux attach -t "$session"
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
# Pretty-print a duration in seconds as "Xs" / "Ym" / "Yh Zm".
|
|
2262
|
+
_loop_runs_dur() {
|
|
2263
|
+
local s="${1:-0}"
|
|
2264
|
+
if [[ "$s" -lt 60 ]]; then printf "%ds" "$s"
|
|
2265
|
+
elif [[ "$s" -lt 3600 ]]; then printf "%dm" "$((s / 60))"
|
|
2266
|
+
else printf "%dh %dm" "$((s / 3600))" "$(((s % 3600) / 60))"
|
|
2267
|
+
fi
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
# Format a single JSONL run record for display.
|
|
2271
|
+
_loop_runs_format_line() {
|
|
2272
|
+
local line="$1" show_project="$2"
|
|
2273
|
+
command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
|
|
2274
|
+
|
|
2275
|
+
local ts status project tcr duration alerts run_id reason built_count built_csv skipped_count
|
|
2276
|
+
ts=$(jq -r '.ts // ""' <<<"$line")
|
|
2277
|
+
status=$(jq -r '.status // ""' <<<"$line")
|
|
2278
|
+
project=$(jq -r '.project // ""' <<<"$line")
|
|
2279
|
+
tcr=$(jq -r '.tcr_count // 0' <<<"$line")
|
|
2280
|
+
duration=$(jq -r '.duration_sec // 0' <<<"$line")
|
|
2281
|
+
alerts=$(jq -r '.alerts // 0' <<<"$line")
|
|
2282
|
+
run_id=$(jq -r '.run_id // ""' <<<"$line")
|
|
2283
|
+
reason=$(jq -r '.reason // ""' <<<"$line")
|
|
2284
|
+
built_count=$(jq -r '(.built // []) | length' <<<"$line")
|
|
2285
|
+
built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
|
|
2286
|
+
skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
|
|
2287
|
+
|
|
2288
|
+
local hhmm; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
|
|
2289
|
+
local prefix=""
|
|
2290
|
+
if [[ "$show_project" == "true" ]]; then
|
|
2291
|
+
prefix="[$(basename "$project")] "
|
|
2292
|
+
fi
|
|
2293
|
+
|
|
2294
|
+
case "$status" in
|
|
2295
|
+
built)
|
|
2296
|
+
local skipped_note=""
|
|
2297
|
+
[[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
|
|
2298
|
+
printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
|
|
2299
|
+
"$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
|
|
2300
|
+
;;
|
|
2301
|
+
idle)
|
|
2302
|
+
printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
|
|
2303
|
+
;;
|
|
2304
|
+
failed)
|
|
2305
|
+
local msg="${reason:-unknown}"
|
|
2306
|
+
printf " %s %s✗ FAILED — %s\n" "$hhmm" "$prefix" "$msg"
|
|
2307
|
+
;;
|
|
2308
|
+
*)
|
|
2309
|
+
printf " %s %s? %s\n" "$hhmm" "$prefix" "$status"
|
|
2310
|
+
;;
|
|
2311
|
+
esac
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
# `roll loop runs [N] [--all]` — show recent loop iteration summaries.
|
|
2315
|
+
_loop_runs() {
|
|
2316
|
+
local n=10 all_flag=false
|
|
2317
|
+
while [[ $# -gt 0 ]]; do
|
|
2318
|
+
case "$1" in
|
|
2319
|
+
--all) all_flag=true; shift ;;
|
|
2320
|
+
[0-9]*) n="$1"; shift ;;
|
|
2321
|
+
*) shift ;;
|
|
2322
|
+
esac
|
|
2323
|
+
done
|
|
2324
|
+
|
|
2325
|
+
if [[ ! -f "$_LOOP_RUNS" ]] || [[ ! -s "$_LOOP_RUNS" ]]; then
|
|
2326
|
+
echo "No loop runs yet 尚无 loop 运行记录"
|
|
2327
|
+
return 0
|
|
2328
|
+
fi
|
|
2329
|
+
|
|
2330
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
2331
|
+
err "jq required for 'roll loop runs' 需要 jq"
|
|
2332
|
+
return 1
|
|
2333
|
+
fi
|
|
2334
|
+
|
|
2335
|
+
local project_path; project_path=$(pwd -P)
|
|
2336
|
+
local filtered
|
|
2337
|
+
if $all_flag; then
|
|
2338
|
+
filtered=$(cat "$_LOOP_RUNS")
|
|
2339
|
+
else
|
|
2340
|
+
filtered=$(jq -c --arg p "$project_path" 'select(.project == $p)' "$_LOOP_RUNS")
|
|
2341
|
+
fi
|
|
2342
|
+
|
|
2343
|
+
if [[ -z "$filtered" ]]; then
|
|
2344
|
+
echo "No loop runs for current project 当前项目尚无 loop 运行记录"
|
|
2345
|
+
return 0
|
|
2346
|
+
fi
|
|
2347
|
+
|
|
2348
|
+
local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
|
|
2349
|
+
local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
|
|
2350
|
+
|
|
2351
|
+
while IFS= read -r line; do
|
|
2352
|
+
[[ -z "$line" ]] && continue
|
|
2353
|
+
_loop_runs_format_line "$line" "$all_flag"
|
|
2354
|
+
done <<<"$recent"
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2135
2357
|
# Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
|
|
2136
2358
|
_loop_tcr_count() {
|
|
2137
2359
|
local started_at="$1"
|
|
@@ -2454,13 +2676,28 @@ _dashboard() {
|
|
|
2454
2676
|
if $_dash_loop_enabled; then
|
|
2455
2677
|
echo -e " Loop ${GREEN}● on${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2456
2678
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2679
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
2680
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
2681
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
2682
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
2683
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
2684
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
2685
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
2686
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
2687
|
+
local loop_sched dream_sched brief_sched
|
|
2688
|
+
loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
|
|
2689
|
+
dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
|
|
2690
|
+
brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
|
|
2691
|
+
local svcs=("loop" "dream" "brief")
|
|
2692
|
+
local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
|
|
2693
|
+
for i in "${!svcs[@]}"; do
|
|
2694
|
+
local svc="${svcs[$i]}" schedule="${scheds[$i]}"
|
|
2695
|
+
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
2696
|
+
case "$state" in
|
|
2697
|
+
enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
|
|
2698
|
+
installed-off) printf " ${YELLOW}%-8s ⚠ off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
|
|
2699
|
+
not-installed) printf " ${RED}%-8s ○ missing${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
|
|
2700
|
+
esac
|
|
2464
2701
|
done
|
|
2465
2702
|
fi
|
|
2466
2703
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.511.
|
|
3
|
+
"version": "2026.511.7",
|
|
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"
|
|
@@ -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
|
|
|
@@ -483,7 +483,7 @@ Both locations must be updated — neither can be skipped:
|
|
|
483
483
|
| [US-{ID}](docs/features/<feature>.md#us-{id}) | {Title} | ✅ Done |
|
|
484
484
|
```
|
|
485
485
|
|
|
486
|
-
Change the Status from `📋 Todo` to `✅ Done`.
|
|
486
|
+
Change the Status from `📋 Todo` or `🔨 In Progress` (whichever the row currently shows) to `✅ Done`. When invoked by `roll-loop`, the row will already be `🔨 In Progress` — that is the expected starting state, and the transition is the same Edit operation.
|
|
487
487
|
For Fly mode: first append an index row under the appropriate Epic > Feature group, then mark it done.
|
|
488
488
|
|
|
489
489
|
**② Update `docs/features/<feature>.md` US section:**
|
package/skills/roll-fix/SKILL.md
CHANGED
|
@@ -296,7 +296,7 @@ Both locations must be updated — neither can be skipped:
|
|
|
296
296
|
| [FIX-{ID}](docs/features/<feature>.md#fix-{id}) | {Title} | ✅ Done |
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
Change the Status of the corresponding row from `📋 Todo` to `✅ Done`.
|
|
299
|
+
Change the Status of the corresponding row from `📋 Todo` or `🔨 In Progress` (whichever the row currently shows) to `✅ Done`. When invoked by `roll-loop`, the row will already be `🔨 In Progress` — that is the expected starting state.
|
|
300
300
|
|
|
301
301
|
**② Update `docs/features/<feature>.md` FIX section:**
|
|
302
302
|
|
|
@@ -57,17 +57,56 @@ if [ -f "$STATE_FILE" ] && grep -q "status: interrupted" "$STATE_FILE"; then
|
|
|
57
57
|
fi
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
**Orphan 🔨 recovery** — clean up stories left in `🔨 In Progress` by a crashed previous run:
|
|
61
|
+
|
|
62
|
+
1. Scan BACKLOG.md for all rows whose Status column contains `🔨 In Progress`.
|
|
63
|
+
2. For each such story, check `state.yaml`:
|
|
64
|
+
- If `current_item` matches the story id AND `status: running` → this is the resume case (handled above), leave it.
|
|
65
|
+
- Otherwise → this is an **orphan 🔨** (the loop that marked it crashed before finishing). Revert the status back to `📋 Todo`, commit `chore: revert orphan 🔨 US-XXX to 📋`, and append a line to `~/.shared/roll/loop/ALERT.md` recording the orphan id and time so the next brief surfaces it.
|
|
66
|
+
3. After orphan sweep, proceed to Step 2 (Scan BACKLOG) — the reverted stories will be picked up normally if they're top of the queue.
|
|
67
|
+
|
|
60
68
|
### Step 2 — Scan BACKLOG
|
|
61
69
|
|
|
62
70
|
Read `BACKLOG.md`. Collect all rows where Status = `📋 Todo`, in order:
|
|
63
71
|
|
|
64
72
|
Priority: FIX-XXX first (bugs block progress), then US-XXX, then REFACTOR-XXX.
|
|
65
73
|
|
|
74
|
+
**Skip rows with Status = `🔨 In Progress`**. These are currently being executed by:
|
|
75
|
+
- Another concurrent executor (human via `$roll-build`, peer agent)
|
|
76
|
+
- An earlier loop iteration that hasn't finished yet (rare; should be guarded by LOCK)
|
|
77
|
+
- A previous interrupted run (the resume logic in Step 1 will pick these up)
|
|
78
|
+
|
|
66
79
|
Cap at `max_items_per_run` to limit blast radius per cycle.
|
|
67
80
|
|
|
81
|
+
### Concurrency Safety
|
|
82
|
+
|
|
83
|
+
Loop has two layers of concurrency protection:
|
|
84
|
+
|
|
85
|
+
1. **Per-project LOCK** (enforced by runner script, see `bin/roll:_write_loop_runner_script`):
|
|
86
|
+
- LOCK file path: `~/.shared/roll/loop/.LOCK-<project-slug>`
|
|
87
|
+
- On launch: if LOCK exists and the PID inside is alive → exit 0 (previous loop still running)
|
|
88
|
+
- On launch: if LOCK exists but PID is dead → clean up stale LOCK and continue
|
|
89
|
+
- On exit (normal or via trap): LOCK is removed
|
|
90
|
+
- One LOCK per project — different projects' loops run independently
|
|
91
|
+
|
|
92
|
+
2. **🔨 In Progress story status** (enforced here):
|
|
93
|
+
- Before picking a story, check its status is `📋 Todo`
|
|
94
|
+
- Skip any `🔨 In Progress` row (someone else is on it)
|
|
95
|
+
- Mark each story `🔨 In Progress` BEFORE invoking the executor skill (see Step 3)
|
|
96
|
+
- On completion: update to `✅ Done`; on TCR failure: revert to `📋 Todo`
|
|
97
|
+
|
|
98
|
+
Together these mean: only one loop runs at a time per project (LOCK), and within a loop, stories already claimed by humans or peer agents are skipped (status check).
|
|
99
|
+
|
|
68
100
|
### Step 3 — Route and Execute
|
|
69
101
|
|
|
70
|
-
For each item:
|
|
102
|
+
For each item, **before invoking the executor skill**, mark the story 🔨 In Progress in BACKLOG.md so brief and peer agents can see it's being worked on:
|
|
103
|
+
|
|
104
|
+
1. Edit BACKLOG.md: change the row's Status column from `📋 Todo` to `🔨 In Progress`.
|
|
105
|
+
2. Commit: `git commit -am "chore: mark US-XXX in progress"` (use the actual story id).
|
|
106
|
+
|
|
107
|
+
This commit is what makes the work visible — without it, tcr micro-commits during execution are invisible to `roll-brief`.
|
|
108
|
+
|
|
109
|
+
Then invoke the executor:
|
|
71
110
|
|
|
72
111
|
```
|
|
73
112
|
Item type → Skill invoked
|
|
@@ -77,7 +116,9 @@ FIX-XXX → Skill("roll-fix", "FIX-XXX")
|
|
|
77
116
|
REFACTOR-XXX → Skill("roll-build", "REFACTOR-XXX")
|
|
78
117
|
```
|
|
79
118
|
|
|
80
|
-
|
|
119
|
+
The executor will update the row to `✅ Done` on success (it transitions from `🔨 In Progress` → `✅ Done`, same Edit logic as from `📋 Todo`).
|
|
120
|
+
|
|
121
|
+
Before invoking, also write current item to state file:
|
|
81
122
|
|
|
82
123
|
```yaml
|
|
83
124
|
# ~/.shared/roll/loop/state.yaml
|
|
@@ -112,6 +153,67 @@ last_run_items: [US-AUTH-003, FIX-007]
|
|
|
112
153
|
last_run_outcome: success
|
|
113
154
|
```
|
|
114
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
|
+
|
|
115
217
|
## Failure Handling
|
|
116
218
|
|
|
117
219
|
### Network Error (transient)
|
|
@@ -194,6 +296,38 @@ roll loop status # show current state
|
|
|
194
296
|
roll loop now # execute one cycle immediately
|
|
195
297
|
```
|
|
196
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
|
+
|
|
197
331
|
## Integration Map
|
|
198
332
|
|
|
199
333
|
```
|