@seanyao/roll 2026.511.6 → 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 +12 -0
- package/bin/roll +215 -4
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +17 -15
- package/skills/roll-loop/SKILL.md +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
3
15
|
## v2026.511.8
|
|
4
16
|
- **Fixed**: 集成测试 launchd ghost 泄漏 — `integration_teardown` 在删除 TEST_TMP 之前,先 `launchctl bootout` 该沙箱里被 `roll loop on` 注册到 user gui domain 的所有 `com.roll.*` 服务,避免删 plist 后 launchd 仍保留指向不存在路径的 ghost 注册。
|
|
5
17
|
|
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,26 +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}"
|
|
1859
1909
|
if [ -f "\$LOCK" ]; then
|
|
1860
1910
|
prev_pid=\$(head -1 "\$LOCK" 2>/dev/null || echo "")
|
|
1861
1911
|
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" >> "
|
|
1912
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "\$LOG"
|
|
1863
1913
|
exit 0
|
|
1864
1914
|
fi
|
|
1865
1915
|
rm -f "\$LOCK"
|
|
1866
1916
|
fi
|
|
1867
1917
|
echo "\$\$" > "\$LOCK"
|
|
1868
1918
|
trap 'rm -f "\$LOCK"' EXIT
|
|
1869
|
-
|
|
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
|
|
1870
1939
|
SCRIPT
|
|
1871
1940
|
chmod +x "$script_path"
|
|
1872
1941
|
}
|
|
@@ -1978,9 +2047,13 @@ cmd_loop() {
|
|
|
1978
2047
|
now) _loop_now ;;
|
|
1979
2048
|
status) _loop_status ;;
|
|
1980
2049
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
2050
|
+
runs) _loop_runs "$@" ;;
|
|
2051
|
+
attach) _loop_attach ;;
|
|
2052
|
+
mute) _loop_mute ;;
|
|
2053
|
+
unmute) _loop_unmute ;;
|
|
1981
2054
|
resume) _loop_resume ;;
|
|
1982
2055
|
reset) _loop_reset ;;
|
|
1983
|
-
*) 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 ;;
|
|
1984
2057
|
esac
|
|
1985
2058
|
}
|
|
1986
2059
|
|
|
@@ -2118,6 +2191,12 @@ _loop_status() {
|
|
|
2118
2191
|
echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
|
|
2119
2192
|
fi
|
|
2120
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
|
|
2121
2200
|
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
|
|
2122
2201
|
[[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
|
|
2123
2202
|
echo ""
|
|
@@ -2143,6 +2222,138 @@ _loop_reset() {
|
|
|
2143
2222
|
fi
|
|
2144
2223
|
}
|
|
2145
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
|
+
|
|
2146
2357
|
# Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
|
|
2147
2358
|
_loop_tcr_count() {
|
|
2148
2359
|
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.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
|
|
|
@@ -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
|
```
|