@seanyao/roll 2026.511.7 → 2026.512.1
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 -33
- package/bin/roll +256 -28
- package/lib/loop-fmt.py +180 -0
- package/package.json +2 -1
- package/skills/roll-.changelog/SKILL.md +70 -41
- package/skills/roll-.dream/SKILL.md +16 -0
- package/skills/roll-brief/SKILL.md +13 -0
- package/skills/roll-design/SKILL.md +89 -2
- package/skills/roll-loop/SKILL.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,52 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v2026.
|
|
4
|
-
- **Added**:
|
|
5
|
-
- **Added**: `roll
|
|
6
|
-
- **
|
|
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 注册。
|
|
3
|
+
## v2026.512.1
|
|
4
|
+
- **Added**: `roll loop pause` / `roll loop resume` — 想自己上手时一键暂停 loop,做完再恢复
|
|
5
|
+
- **Added**: `roll status` 新增所有项目的 loop 状态一览 — 调度时间、待办数、是否在跑
|
|
6
|
+
- **Fixed**: `roll loop attach` 不再黑屏,AI 干活过程实时可见
|
|
17
7
|
|
|
18
8
|
## v2026.511.7
|
|
19
|
-
- **Added**: loop
|
|
20
|
-
- **Added**: loop
|
|
21
|
-
- **
|
|
9
|
+
- **Added**: loop 跑起来时自动弹出一个终端窗口,看 AI 实时干活
|
|
10
|
+
- **Added**: `roll loop mute` 关掉自动弹窗,`roll loop unmute` 恢复
|
|
11
|
+
- **Added**: `roll loop runs` — 查看 loop 最近几次都跑了什么
|
|
12
|
+
- **Added**: `roll loop attach` — 随时接入正在跑的 loop 现场围观
|
|
13
|
+
- **Added**: BACKLOG 任务执行中会实时显示 🔨 进度,不用等做完才知道
|
|
14
|
+
- **Added**: `roll setup` 自动安装 tmux(macOS 通过 brew)
|
|
15
|
+
- **Improved**: 代码巡检(dream)报告改为中文输出
|
|
16
|
+
- **Fixed**: loop 在某些情况下完成后不正常退出
|
|
17
|
+
- **Fixed**: loop 中途崩溃后下次启动会自动清理残留状态
|
|
22
18
|
|
|
23
19
|
## v2026.511.6
|
|
24
|
-
- **
|
|
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 的新结构。
|
|
20
|
+
- **Fixed**: 多个 loop 实例不会再因为定时重复触发而互相打架
|
|
27
21
|
|
|
28
22
|
## v2026.511.5
|
|
29
|
-
- **Fixed**:
|
|
30
|
-
- **Improved**: roll loop status
|
|
23
|
+
- **Fixed**: 升级 roll 后 loop 服务自动生效,无需手动重启
|
|
24
|
+
- **Improved**: `roll loop status` 三态显示,看得清是真没装、装了没启、还是在跑
|
|
31
25
|
|
|
32
26
|
## v2026.511.4
|
|
33
|
-
- **Fixed**:
|
|
27
|
+
- **Fixed**: 升级 roll 后 `roll init` 自动迁移 loop 配置,少一步手动操作
|
|
34
28
|
|
|
35
29
|
## v2026.511.3
|
|
36
|
-
- **Fixed**:
|
|
37
|
-
- **Fixed**:
|
|
30
|
+
- **Fixed**: 多个项目同时跑 loop,互不干扰
|
|
31
|
+
- **Fixed**: 在 roll 项目里运行 `roll release` 会提示改用 `scripts/release.sh`
|
|
38
32
|
|
|
39
33
|
## v2026.511.2
|
|
40
|
-
- **Added**: roll loop monitor
|
|
41
|
-
- **Fixed**: dashboard
|
|
42
|
-
- **Fixed**: loop
|
|
43
|
-
- **
|
|
44
|
-
- **Improved**: roll-brief 输出格式 — 序号命名、省略空 section、元信息格式精简,减少无效噪音
|
|
34
|
+
- **Added**: `roll loop monitor` — 一屏看 loop/dream/brief 三个调度服务状态
|
|
35
|
+
- **Fixed**: dashboard 待办数、release 状态显示问题
|
|
36
|
+
- **Fixed**: loop 异常退出后队列卡住不再继续执行
|
|
37
|
+
- **Improved**: 简报输出更精简,去掉空白段落和冗余信息
|
|
45
38
|
|
|
46
39
|
## v2026.511.1
|
|
47
|
-
- **Changed**:
|
|
48
|
-
- **Added**:
|
|
49
|
-
- **Fixed**: CI 测试环境兼容 — 移除依赖本地 state.yaml 的 hello_world.bats,修复 GitHub Actions 持续失败
|
|
40
|
+
- **Changed**: macOS 上 loop 调度切换到 launchd,比 crontab 更稳定
|
|
41
|
+
- **Added**: agent 跳过 TCR 节奏时自动拦回 Todo,强制重做
|
|
50
42
|
|
|
51
43
|
## v2026.510.10
|
|
52
44
|
- **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.
|
|
7
|
+
VERSION="2026.512.1"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -1254,6 +1254,63 @@ cmd_status() {
|
|
|
1254
1254
|
fi
|
|
1255
1255
|
done
|
|
1256
1256
|
|
|
1257
|
+
_status_loop_overview
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
_status_loop_overview() {
|
|
1261
|
+
[[ "$(uname)" != "Darwin" ]] && return 0
|
|
1262
|
+
|
|
1263
|
+
local plists=()
|
|
1264
|
+
while IFS= read -r p; do
|
|
1265
|
+
[[ -f "$p" ]] && plists+=("$p")
|
|
1266
|
+
done < <(ls "${_LAUNCHD_DIR}"/com.roll.loop.*.plist 2>/dev/null)
|
|
1267
|
+
|
|
1268
|
+
[[ "${#plists[@]}" -eq 0 ]] && return 0
|
|
1269
|
+
|
|
1270
|
+
echo ""
|
|
1271
|
+
echo -e "${BOLD}Loop Overview: 所有项目 loop 状态${NC}"
|
|
1272
|
+
|
|
1273
|
+
for plist in "${plists[@]}"; do
|
|
1274
|
+
local label; label=$(basename "$plist" .plist)
|
|
1275
|
+
|
|
1276
|
+
local proj_path
|
|
1277
|
+
proj_path=$(awk '/<key>WorkingDirectory<\/key>/{f=1;next} f{gsub(/^[[:space:]]*<string>|<\/string>[[:space:]]*$/,"");print;exit}' "$plist" 2>/dev/null)
|
|
1278
|
+
|
|
1279
|
+
local proj_name path_note=""
|
|
1280
|
+
if [[ -n "$proj_path" && -d "$proj_path" ]]; then
|
|
1281
|
+
proj_name=$(basename "$proj_path")
|
|
1282
|
+
elif [[ -n "$proj_path" ]]; then
|
|
1283
|
+
proj_name=$(basename "$proj_path")
|
|
1284
|
+
path_note=" (path missing)"
|
|
1285
|
+
else
|
|
1286
|
+
proj_name="(unknown)"
|
|
1287
|
+
fi
|
|
1288
|
+
|
|
1289
|
+
local state_icon
|
|
1290
|
+
if _launchd_is_loaded "$label"; then
|
|
1291
|
+
state_icon="${GREEN}●${NC}"
|
|
1292
|
+
else
|
|
1293
|
+
state_icon="${RED}○${NC}"
|
|
1294
|
+
fi
|
|
1295
|
+
|
|
1296
|
+
local minute hour schedule
|
|
1297
|
+
minute=$(awk '/<key>Minute<\/key>/{f=1;next} f{gsub(/^[[:space:]]*<integer>|<\/integer>[[:space:]]*$/,"");print;exit}' "$plist" 2>/dev/null)
|
|
1298
|
+
hour=$(awk '/<key>Hour<\/key>/{f=1;next} f{gsub(/^[[:space:]]*<integer>|<\/integer>[[:space:]]*$/,"");print;exit}' "$plist" 2>/dev/null)
|
|
1299
|
+
if [[ -n "$hour" && -n "$minute" ]]; then
|
|
1300
|
+
schedule=$(printf "%02d:%02d" "$hour" "$minute")
|
|
1301
|
+
elif [[ -n "$minute" ]]; then
|
|
1302
|
+
schedule=":$(printf '%02d' "$minute")"
|
|
1303
|
+
else
|
|
1304
|
+
schedule="?"
|
|
1305
|
+
fi
|
|
1306
|
+
|
|
1307
|
+
local todo_count=0
|
|
1308
|
+
if [[ -z "$path_note" && -f "${proj_path}/BACKLOG.md" ]]; then
|
|
1309
|
+
todo_count=$(grep -c '📋 Todo' "${proj_path}/BACKLOG.md" 2>/dev/null; true)
|
|
1310
|
+
fi
|
|
1311
|
+
|
|
1312
|
+
echo -e " ${state_icon} ${proj_name}${path_note} ${schedule} ${todo_count} pending"
|
|
1313
|
+
done
|
|
1257
1314
|
}
|
|
1258
1315
|
|
|
1259
1316
|
check_sync_status() {
|
|
@@ -1726,7 +1783,7 @@ _agent_run_skill() {
|
|
|
1726
1783
|
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill}"; return 1; }
|
|
1727
1784
|
local content; content=$(_skill_content "$skill_file")
|
|
1728
1785
|
case "$agent" in
|
|
1729
|
-
claude) claude -p "$content" ;;
|
|
1786
|
+
claude) claude -p --output-format stream-json "$content" ;;
|
|
1730
1787
|
kimi) kimi --quiet -p "$content" ;;
|
|
1731
1788
|
deepseek) deepseek "$content" ;;
|
|
1732
1789
|
pi) pi -p "$content" ;;
|
|
@@ -1810,6 +1867,13 @@ _config_read_int() {
|
|
|
1810
1867
|
if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val"; else echo "$default"; fi
|
|
1811
1868
|
}
|
|
1812
1869
|
|
|
1870
|
+
_config_read_string() {
|
|
1871
|
+
local key="$1" default="$2"
|
|
1872
|
+
local val
|
|
1873
|
+
val=$(grep "^${key}:" "$ROLL_CONFIG" 2>/dev/null | awk '{print $2}' | tr -d '"' | head -1)
|
|
1874
|
+
if [[ -n "$val" ]]; then echo "$val"; else echo "$default"; fi
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1813
1877
|
# Derive a minute in [1,55] from project path hash + offset so different projects
|
|
1814
1878
|
# and different services within a project don't fire at the same time.
|
|
1815
1879
|
# Offsets used: loop=0, dream=2, brief=4 → always three distinct values (2<55).
|
|
@@ -1887,21 +1951,37 @@ _write_runner_script() {
|
|
|
1887
1951
|
# Falls back to headless execution when tmux is not installed.
|
|
1888
1952
|
_write_loop_runner_script() {
|
|
1889
1953
|
local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
|
|
1890
|
-
local active_start="${5:-10}" active_end="${6:-18}"
|
|
1954
|
+
local active_start="${5:-10}" active_end="${6:-18}" terminal_pref="${7:-Terminal}"
|
|
1891
1955
|
mkdir -p "$(dirname "$script_path")"
|
|
1892
1956
|
|
|
1893
1957
|
local inner_path="${script_path%.sh}-inner.sh"
|
|
1958
|
+
# Use stream-json + formatter: --verbose alone does nothing in -p mode;
|
|
1959
|
+
# stream-json enables realtime streaming; loop-fmt.py humanizes the events.
|
|
1960
|
+
local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
|
|
1961
|
+
local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
|
|
1894
1962
|
cat > "$inner_path" << INNER
|
|
1895
1963
|
#!/bin/bash -l
|
|
1896
1964
|
export PATH="/opt/homebrew/bin:\$PATH"
|
|
1897
|
-
|
|
1965
|
+
FMT="${fmt_script}"
|
|
1966
|
+
if [ -f "\$FMT" ]; then
|
|
1967
|
+
( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
|
|
1968
|
+
else
|
|
1969
|
+
cd "${project_path}" && ${cmd_verbose}
|
|
1970
|
+
fi
|
|
1898
1971
|
INNER
|
|
1899
1972
|
chmod +x "$inner_path"
|
|
1900
1973
|
|
|
1974
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
1901
1975
|
cat > "$script_path" << SCRIPT
|
|
1902
1976
|
#!/bin/bash -l
|
|
1903
|
-
|
|
1904
|
-
if [
|
|
1977
|
+
# Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
|
|
1978
|
+
if [ -z "\$ROLL_LOOP_FORCE" ]; then
|
|
1979
|
+
h=\$(printf '%d' "\$(date +%H)")
|
|
1980
|
+
if [ "\$h" -lt ${active_start} ] || [ "\$h" -ge ${active_end} ]; then exit 0; fi
|
|
1981
|
+
fi
|
|
1982
|
+
# Pause check — 'roll loop pause' creates this marker to suspend scheduling
|
|
1983
|
+
PAUSE="\$HOME/.shared/roll/loop/PAUSE-${slug}"
|
|
1984
|
+
if [ -z "\$ROLL_LOOP_FORCE" ] && [ -f "\$PAUSE" ]; then exit 0; fi
|
|
1905
1985
|
LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1906
1986
|
SESSION="roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1907
1987
|
INNER_SCRIPT="${inner_path}"
|
|
@@ -1923,14 +2003,30 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
1923
2003
|
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
1924
2004
|
# tmux session so the user can watch the loop work in real time. Best-effort
|
|
1925
2005
|
# focus retention: capture the current frontmost app and re-activate after.
|
|
1926
|
-
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2006
|
+
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2007
|
+
case "${terminal_pref}" in
|
|
2008
|
+
ghostty|Ghostty)
|
|
2009
|
+
(open -na Ghostty.app --args -e tmux attach -t \$SESSION >/dev/null 2>&1) &
|
|
2010
|
+
;;
|
|
2011
|
+
iTerm2|iTerm)
|
|
2012
|
+
(osascript \\
|
|
2013
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2014
|
+
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
|
|
2015
|
+
-e 'delay 0.3' \\
|
|
2016
|
+
-e 'try' \\
|
|
2017
|
+
-e 'tell application _prev to activate' \\
|
|
2018
|
+
-e 'end try' >/dev/null 2>&1) &
|
|
2019
|
+
;;
|
|
2020
|
+
*)
|
|
2021
|
+
command -v osascript >/dev/null 2>&1 && (osascript \\
|
|
2022
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2023
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
2024
|
+
-e 'delay 0.3' \\
|
|
2025
|
+
-e 'try' \\
|
|
2026
|
+
-e 'tell application _prev to activate' \\
|
|
2027
|
+
-e 'end try' >/dev/null 2>&1) &
|
|
2028
|
+
;;
|
|
2029
|
+
esac
|
|
1934
2030
|
fi
|
|
1935
2031
|
while tmux has-session -t "\$SESSION" 2>/dev/null; do sleep 5; done
|
|
1936
2032
|
else
|
|
@@ -1978,6 +2074,17 @@ _install_launchd_plists() {
|
|
|
1978
2074
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
1979
2075
|
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
1980
2076
|
|
|
2077
|
+
# Terminal preference: config wins, then TERM_PROGRAM env, then "Terminal"
|
|
2078
|
+
local terminal_pref
|
|
2079
|
+
terminal_pref=$(_config_read_string "loop_attach_terminal" "")
|
|
2080
|
+
if [[ -z "$terminal_pref" ]]; then
|
|
2081
|
+
case "${TERM_PROGRAM:-}" in
|
|
2082
|
+
ghostty) terminal_pref="ghostty" ;;
|
|
2083
|
+
iTerm.app) terminal_pref="iTerm2" ;;
|
|
2084
|
+
*) terminal_pref="Terminal" ;;
|
|
2085
|
+
esac
|
|
2086
|
+
fi
|
|
2087
|
+
|
|
1981
2088
|
local services=("loop" "dream" "brief")
|
|
1982
2089
|
local skill_names=("roll-loop" "roll-.dream" "roll-brief")
|
|
1983
2090
|
local minutes=("$loop_minute" "$dream_minute" "$brief_minute")
|
|
@@ -1997,7 +2104,7 @@ _install_launchd_plists() {
|
|
|
1997
2104
|
local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
|
|
1998
2105
|
|
|
1999
2106
|
if [[ "$svc" == "loop" ]]; then
|
|
2000
|
-
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
|
|
2107
|
+
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end" "$terminal_pref"
|
|
2001
2108
|
else
|
|
2002
2109
|
_write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
|
|
2003
2110
|
fi
|
|
@@ -2045,15 +2152,17 @@ cmd_loop() {
|
|
|
2045
2152
|
on) _loop_on ;;
|
|
2046
2153
|
off) _loop_off ;;
|
|
2047
2154
|
now) _loop_now ;;
|
|
2155
|
+
test) _loop_test ;;
|
|
2048
2156
|
status) _loop_status ;;
|
|
2049
2157
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
2050
2158
|
runs) _loop_runs "$@" ;;
|
|
2051
2159
|
attach) _loop_attach ;;
|
|
2052
2160
|
mute) _loop_mute ;;
|
|
2053
2161
|
unmute) _loop_unmute ;;
|
|
2162
|
+
pause) _loop_pause ;;
|
|
2054
2163
|
resume) _loop_resume ;;
|
|
2055
2164
|
reset) _loop_reset ;;
|
|
2056
|
-
*) err "Usage: roll loop <on|off|now|status|monitor|runs|attach|mute|unmute|resume|reset>"; exit 1 ;;
|
|
2165
|
+
*) err "Usage: roll loop <on|off|now|test|status|monitor|runs|attach|mute|unmute|pause|resume|reset>"; exit 1 ;;
|
|
2057
2166
|
esac
|
|
2058
2167
|
}
|
|
2059
2168
|
|
|
@@ -2158,34 +2267,108 @@ _loop_now() {
|
|
|
2158
2267
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
2159
2268
|
warn "Loop already running loop 正在运行中"; return 0
|
|
2160
2269
|
fi
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2270
|
+
# Invoke the SAME runner script that launchd would invoke — same tmux,
|
|
2271
|
+
# same --verbose, same LOCK, same auto-attach popup. ROLL_LOOP_FORCE
|
|
2272
|
+
# bypasses only the active-window check (manual triggers aren't time-gated).
|
|
2273
|
+
local project_path; project_path=$(pwd -P)
|
|
2274
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2275
|
+
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
2276
|
+
if [[ ! -f "$runner" ]]; then
|
|
2277
|
+
err "Runner script not found: ${runner}"
|
|
2278
|
+
err "Run 'roll setup' or 'roll loop on' first to generate it."
|
|
2279
|
+
return 1
|
|
2280
|
+
fi
|
|
2281
|
+
info "Starting new loop cycle... 正在启动新的循环..."
|
|
2282
|
+
ROLL_LOOP_FORCE=1 bash "$runner"
|
|
2283
|
+
# Reset stale running state if the cycle exited without cleanup (e.g. API error, SIGKILL)
|
|
2166
2284
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "^status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
2167
2285
|
printf "status: idle\n" > "$_LOOP_STATE"
|
|
2168
2286
|
fi
|
|
2169
2287
|
}
|
|
2170
2288
|
|
|
2289
|
+
_loop_test() {
|
|
2290
|
+
local project_path; project_path=$(pwd -P)
|
|
2291
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2292
|
+
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
2293
|
+
if [[ ! -f "$runner" ]]; then
|
|
2294
|
+
err "Runner not found: ${runner}"
|
|
2295
|
+
err "Run 'roll loop on' first to generate it."
|
|
2296
|
+
return 1
|
|
2297
|
+
fi
|
|
2298
|
+
local log="${_SHARED_ROOT}/loop/cron.log"
|
|
2299
|
+
local test_runner="${_SHARED_ROOT}/loop/run-${slug}-test.sh"
|
|
2300
|
+
|
|
2301
|
+
# Detect terminal pref same way _install_launchd_plists does
|
|
2302
|
+
local terminal_pref
|
|
2303
|
+
terminal_pref=$(_config_read_string "loop_attach_terminal" "")
|
|
2304
|
+
if [[ -z "$terminal_pref" ]]; then
|
|
2305
|
+
case "${TERM_PROGRAM:-}" in
|
|
2306
|
+
ghostty) terminal_pref="ghostty" ;;
|
|
2307
|
+
iTerm.app) terminal_pref="iTerm2" ;;
|
|
2308
|
+
*) terminal_pref="Terminal" ;;
|
|
2309
|
+
esac
|
|
2310
|
+
fi
|
|
2311
|
+
|
|
2312
|
+
local active_start active_end
|
|
2313
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
2314
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
2315
|
+
|
|
2316
|
+
info "Generating test runner... 正在生成测试启动脚本..."
|
|
2317
|
+
_write_loop_runner_script "$test_runner" "$project_path" \
|
|
2318
|
+
'claude -p "Reply with a single word: hello"; sleep 10' \
|
|
2319
|
+
"$log" "$active_start" "$active_end" "$terminal_pref"
|
|
2320
|
+
|
|
2321
|
+
info "Starting smoke test (real claude, trivial prompt)... 正在运行 smoke 测试(真实 claude)..."
|
|
2322
|
+
info "Watch for: tmux session + terminal popup + stream-json events flowing"
|
|
2323
|
+
info "观察:tmux 会话 + 终端弹窗 + stream-json 事件流"
|
|
2324
|
+
|
|
2325
|
+
local start_time; start_time=$(date +%s)
|
|
2326
|
+
ROLL_LOOP_FORCE=1 bash "$test_runner"
|
|
2327
|
+
local exit_code=$?
|
|
2328
|
+
local elapsed=$(( $(date +%s) - start_time ))
|
|
2329
|
+
|
|
2330
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
2331
|
+
ok "Smoke test passed (${elapsed}s) smoke 测试通过 (${elapsed}秒)"
|
|
2332
|
+
else
|
|
2333
|
+
err "Smoke test failed (exit ${exit_code}, ${elapsed}s) smoke 测试失败 (退出码 ${exit_code}, ${elapsed}秒)"
|
|
2334
|
+
return 1
|
|
2335
|
+
fi
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2171
2338
|
_loop_status() {
|
|
2172
2339
|
local project_path; project_path=$(pwd -P)
|
|
2173
2340
|
local agent; agent=$(_project_agent)
|
|
2341
|
+
local _is_paused=false
|
|
2342
|
+
[[ -f "$_LOOP_STATE" ]] && grep -q "^status: paused" "$_LOOP_STATE" 2>/dev/null && _is_paused=true
|
|
2174
2343
|
echo ""
|
|
2175
2344
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2176
2345
|
echo -e " Services Agent: ${CYAN}${agent}${NC}"
|
|
2177
2346
|
for svc in loop dream brief; do
|
|
2178
2347
|
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2348
|
+
if [[ "$svc" == "loop" ]] && $_is_paused; then
|
|
2349
|
+
local _paused_at; _paused_at=$(grep '^paused_at:' "$_LOOP_STATE" 2>/dev/null | awk '{print $2}' | tr -d '"')
|
|
2350
|
+
local _dur=""
|
|
2351
|
+
if [[ -n "$_paused_at" ]]; then
|
|
2352
|
+
local _pe _ne _sec
|
|
2353
|
+
_pe=$(date -d "$_paused_at" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$_paused_at" +%s 2>/dev/null || echo 0)
|
|
2354
|
+
_ne=$(date +%s); _sec=$(( _ne - _pe ))
|
|
2355
|
+
_dur=" ($(( _sec / 3600 ))h $(( (_sec % 3600) / 60 ))m ago)"
|
|
2356
|
+
fi
|
|
2357
|
+
echo -e " ${YELLOW}loop ⏸ paused${NC}${_dur} run: roll loop resume"
|
|
2358
|
+
else
|
|
2359
|
+
case "$state" in
|
|
2360
|
+
enabled) echo -e " ${GREEN}${svc} ● enabled${NC}" ;;
|
|
2361
|
+
installed-off) echo -e " ${YELLOW}${svc} ⚠ installed/off${NC} run: roll loop on" ;;
|
|
2362
|
+
not-installed) echo -e " ${RED}${svc} ○ not installed${NC} run: roll setup" ;;
|
|
2363
|
+
esac
|
|
2364
|
+
fi
|
|
2184
2365
|
done
|
|
2185
2366
|
else
|
|
2186
2367
|
local loop_enabled=false
|
|
2187
2368
|
crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_enabled=true
|
|
2188
|
-
if $
|
|
2369
|
+
if $_is_paused; then
|
|
2370
|
+
echo -e " Scheduler ${YELLOW}⏸ paused${NC} run: roll loop resume"
|
|
2371
|
+
elif $loop_enabled; then
|
|
2189
2372
|
echo -e " Scheduler ${GREEN}● enabled${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2190
2373
|
else
|
|
2191
2374
|
echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
|
|
@@ -2202,7 +2385,47 @@ _loop_status() {
|
|
|
2202
2385
|
echo ""
|
|
2203
2386
|
}
|
|
2204
2387
|
|
|
2388
|
+
_loop_pause() {
|
|
2389
|
+
local project_path; project_path=$(pwd -P)
|
|
2390
|
+
local paused_at; paused_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
2391
|
+
|
|
2392
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2393
|
+
local label; label=$(_launchd_label "loop" "$project_path")
|
|
2394
|
+
if ! _launchd_is_loaded "$label"; then
|
|
2395
|
+
warn "Loop not enabled — nothing to pause loop 未启用,无需暂停"; return 0
|
|
2396
|
+
fi
|
|
2397
|
+
launchctl unload -w "$(_launchd_plist_path "loop" "$project_path")" 2>/dev/null || true
|
|
2398
|
+
else
|
|
2399
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2400
|
+
touch "${_SHARED_ROOT}/loop/PAUSE-${slug}"
|
|
2401
|
+
fi
|
|
2402
|
+
|
|
2403
|
+
mkdir -p "$(dirname "$_LOOP_STATE")"
|
|
2404
|
+
printf 'status: paused\npaused_at: "%s"\npaused_reason: manual\n' "$paused_at" > "$_LOOP_STATE"
|
|
2405
|
+
ok "Loop paused 已暂停 run: roll loop resume"
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2205
2408
|
_loop_resume() {
|
|
2409
|
+
local project_path; project_path=$(pwd -P)
|
|
2410
|
+
|
|
2411
|
+
# Scheduler resume: loop was manually paused via `roll loop pause`
|
|
2412
|
+
if [[ -f "$_LOOP_STATE" ]] && grep -q "^status: paused" "$_LOOP_STATE" 2>/dev/null; then
|
|
2413
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2414
|
+
local label; label=$(_launchd_label "loop" "$project_path")
|
|
2415
|
+
local plist; plist=$(_launchd_plist_path "loop" "$project_path")
|
|
2416
|
+
if [[ -f "$plist" ]]; then
|
|
2417
|
+
launchctl load -w "$plist" 2>/dev/null || true
|
|
2418
|
+
fi
|
|
2419
|
+
else
|
|
2420
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2421
|
+
rm -f "${_SHARED_ROOT}/loop/PAUSE-${slug}"
|
|
2422
|
+
fi
|
|
2423
|
+
printf "status: idle\n" > "$_LOOP_STATE"
|
|
2424
|
+
ok "Loop resumed 已恢复"
|
|
2425
|
+
return 0
|
|
2426
|
+
fi
|
|
2427
|
+
|
|
2428
|
+
# Interrupt resume: loop was running a Story and crashed
|
|
2206
2429
|
if [[ ! -f "$_LOOP_STATE" ]]; then
|
|
2207
2430
|
warn "No loop state found — nothing to resume 未找到 loop 状态,无需恢复"; return 0
|
|
2208
2431
|
fi
|
|
@@ -2667,13 +2890,18 @@ _dashboard() {
|
|
|
2667
2890
|
|
|
2668
2891
|
echo -e "\n ${BOLD}${CYAN}${project_name}${NC} ${YELLOW}v${VERSION}${NC}\n"
|
|
2669
2892
|
|
|
2893
|
+
local _dash_loop_paused=false
|
|
2894
|
+
[[ -f "$_LOOP_STATE" ]] && grep -q "^status: paused" "$_LOOP_STATE" 2>/dev/null && _dash_loop_paused=true
|
|
2895
|
+
|
|
2670
2896
|
local _dash_loop_enabled=false
|
|
2671
2897
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2672
2898
|
_launchd_is_loaded "$(_launchd_label "loop" "$project_path")" && _dash_loop_enabled=true
|
|
2673
2899
|
else
|
|
2674
2900
|
crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && _dash_loop_enabled=true
|
|
2675
2901
|
fi
|
|
2676
|
-
if $
|
|
2902
|
+
if $_dash_loop_paused; then
|
|
2903
|
+
echo -e " Loop ${YELLOW}⏸ paused${NC} run: roll loop resume"
|
|
2904
|
+
elif $_dash_loop_enabled; then
|
|
2677
2905
|
echo -e " Loop ${GREEN}● on${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2678
2906
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2679
2907
|
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
package/lib/loop-fmt.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
loop-fmt.py — stream-json → human-readable formatter for roll loop tmux output.
|
|
4
|
+
|
|
5
|
+
Reads stream-json lines from stdin, emits colored, human-readable events.
|
|
6
|
+
Skips noise (system/init, hook_started, rate_limit_event) and abbreviates
|
|
7
|
+
tool results so the window stays readable.
|
|
8
|
+
|
|
9
|
+
Color codes: no external deps, plain ANSI.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import textwrap
|
|
16
|
+
|
|
17
|
+
# ANSI colors
|
|
18
|
+
CYAN = "\033[36m"
|
|
19
|
+
GREEN = "\033[32m"
|
|
20
|
+
YELLOW = "\033[33m"
|
|
21
|
+
RED = "\033[31m"
|
|
22
|
+
GRAY = "\033[90m"
|
|
23
|
+
BOLD = "\033[1m"
|
|
24
|
+
RESET = "\033[0m"
|
|
25
|
+
DIM = "\033[2m"
|
|
26
|
+
|
|
27
|
+
SKIP_SUBTYPES = {"hook_started", "hook_response", "hook_stop_hook_execution",
|
|
28
|
+
"hook_stop_hook_active_hooks_ran"}
|
|
29
|
+
|
|
30
|
+
def trunc(s, n=120):
|
|
31
|
+
s = str(s).replace("\n", " ").strip()
|
|
32
|
+
return s[:n] + "…" if len(s) > n else s
|
|
33
|
+
|
|
34
|
+
def fmt_tool_input(name, inp):
|
|
35
|
+
if not isinstance(inp, dict):
|
|
36
|
+
return trunc(str(inp), 80)
|
|
37
|
+
if name == "Bash":
|
|
38
|
+
cmd = inp.get("command", "")
|
|
39
|
+
# show first non-empty line
|
|
40
|
+
lines = [l.strip() for l in cmd.splitlines() if l.strip()]
|
|
41
|
+
return trunc(lines[0] if lines else cmd, 100)
|
|
42
|
+
if name in ("Read", "Write", "Edit"):
|
|
43
|
+
path = inp.get("file_path", inp.get("path", ""))
|
|
44
|
+
extra = ""
|
|
45
|
+
if name == "Edit":
|
|
46
|
+
old = inp.get("old_string", "")
|
|
47
|
+
extra = f" ({trunc(old, 40)})"
|
|
48
|
+
return f"{path}{extra}"
|
|
49
|
+
if name in ("Glob", "Grep"):
|
|
50
|
+
return trunc(inp.get("pattern", inp.get("query", str(inp))), 80)
|
|
51
|
+
if name == "Skill":
|
|
52
|
+
return inp.get("skill", "") + (" " + inp.get("args", "") if inp.get("args") else "")
|
|
53
|
+
if name == "Agent":
|
|
54
|
+
return trunc(inp.get("description", str(inp)), 80)
|
|
55
|
+
return trunc(json.dumps(inp, ensure_ascii=False), 80)
|
|
56
|
+
|
|
57
|
+
def fmt_tool_result(content):
|
|
58
|
+
if isinstance(content, list):
|
|
59
|
+
parts = []
|
|
60
|
+
for c in content:
|
|
61
|
+
if isinstance(c, dict):
|
|
62
|
+
t = c.get("type", "")
|
|
63
|
+
if t == "text":
|
|
64
|
+
parts.append(c.get("text", ""))
|
|
65
|
+
elif t == "image":
|
|
66
|
+
parts.append("[image]")
|
|
67
|
+
else:
|
|
68
|
+
parts.append(str(c))
|
|
69
|
+
text = " ".join(parts)
|
|
70
|
+
else:
|
|
71
|
+
text = str(content) if content is not None else ""
|
|
72
|
+
# strip ansi for length check
|
|
73
|
+
clean = re.sub(r'\033\[[0-9;]*m', '', text)
|
|
74
|
+
lines = [l for l in clean.splitlines() if l.strip()]
|
|
75
|
+
if not lines:
|
|
76
|
+
return "(empty)"
|
|
77
|
+
# show first 3 lines, trim long lines
|
|
78
|
+
out = []
|
|
79
|
+
for l in lines[:3]:
|
|
80
|
+
out.append(" " + trunc(l, 100))
|
|
81
|
+
if len(lines) > 3:
|
|
82
|
+
out.append(f" {DIM}… ({len(lines)-3} more lines){RESET}")
|
|
83
|
+
return "\n".join(out)
|
|
84
|
+
|
|
85
|
+
def process_line(line):
|
|
86
|
+
line = line.rstrip()
|
|
87
|
+
if not line:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
ev = json.loads(line)
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
# plain text passthrough
|
|
93
|
+
print(line)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
etype = ev.get("type", "")
|
|
97
|
+
|
|
98
|
+
# ── system events ──────────────────────────────────────────────
|
|
99
|
+
if etype == "system":
|
|
100
|
+
subtype = ev.get("subtype", "")
|
|
101
|
+
if subtype in SKIP_SUBTYPES:
|
|
102
|
+
return
|
|
103
|
+
if subtype == "init":
|
|
104
|
+
model = ev.get("model", "")
|
|
105
|
+
tools = ev.get("tools", [])
|
|
106
|
+
tool_list = ", ".join(tools[:6])
|
|
107
|
+
if len(tools) > 6:
|
|
108
|
+
tool_list += f" +{len(tools)-6}"
|
|
109
|
+
print(f"{DIM}[init] model={model} tools={tool_list}{RESET}")
|
|
110
|
+
return
|
|
111
|
+
# unknown system — show raw briefly
|
|
112
|
+
print(f"{DIM}[sys/{subtype}]{RESET}")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# ── rate limit ────────────────────────────────────────────────
|
|
116
|
+
if etype == "rate_limit_event":
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# ── assistant ────────────────────────────────────────────────
|
|
120
|
+
if etype == "assistant":
|
|
121
|
+
msg = ev.get("message", {})
|
|
122
|
+
for blk in msg.get("content", []):
|
|
123
|
+
btype = blk.get("type", "")
|
|
124
|
+
if btype == "tool_use":
|
|
125
|
+
name = blk.get("name", "?")
|
|
126
|
+
inp = blk.get("input", {})
|
|
127
|
+
summary = fmt_tool_input(name, inp)
|
|
128
|
+
print(f"{CYAN}→ {BOLD}{name}{RESET}{CYAN}: {summary}{RESET}")
|
|
129
|
+
elif btype == "text":
|
|
130
|
+
text = blk.get("text", "").strip()
|
|
131
|
+
if text:
|
|
132
|
+
# wrap long text
|
|
133
|
+
for l in textwrap.wrap(text, 120):
|
|
134
|
+
print(f"{GREEN}{l}{RESET}")
|
|
135
|
+
elif btype == "thinking":
|
|
136
|
+
thought = blk.get("thinking", "").strip()
|
|
137
|
+
if thought:
|
|
138
|
+
first = trunc(thought, 80)
|
|
139
|
+
print(f"{DIM}[thinking] {first}{RESET}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# ── user (tool results) ───────────────────────────────────────
|
|
143
|
+
if etype == "user":
|
|
144
|
+
msg = ev.get("message", {})
|
|
145
|
+
for blk in msg.get("content", []):
|
|
146
|
+
if blk.get("type") == "tool_result":
|
|
147
|
+
is_err = blk.get("is_error", False)
|
|
148
|
+
content = blk.get("content", "")
|
|
149
|
+
result_text = fmt_tool_result(content)
|
|
150
|
+
prefix = f"{RED} ✗{RESET}" if is_err else f"{GRAY} ↩{RESET}"
|
|
151
|
+
print(f"{prefix} {result_text}")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# ── result (final) ───────────────────────────────────────────
|
|
155
|
+
if etype == "result":
|
|
156
|
+
dur_ms = ev.get("duration_ms", 0)
|
|
157
|
+
cost_usd = ev.get("total_cost_usd", 0)
|
|
158
|
+
turns = ev.get("num_turns", "?")
|
|
159
|
+
dur_s = dur_ms / 1000
|
|
160
|
+
cost_str = f"${cost_usd:.4f}" if cost_usd else ""
|
|
161
|
+
subtype = ev.get("subtype", "")
|
|
162
|
+
if subtype == "error_max_turns":
|
|
163
|
+
print(f"{RED}✗ max turns reached {dur_s:.1f}s{RESET}")
|
|
164
|
+
else:
|
|
165
|
+
cost_part = f" {YELLOW}{cost_str}{RESET}" if cost_str else ""
|
|
166
|
+
print(f"\n{GREEN}{BOLD}✓ done{RESET} {dur_s:.1f}s {GRAY}{turns} turns{RESET}{cost_part}")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# ── fallback ────────────────────────────────────────────────
|
|
170
|
+
print(f"{DIM}{trunc(line, 160)}{RESET}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main():
|
|
174
|
+
for line in sys.stdin:
|
|
175
|
+
process_line(line)
|
|
176
|
+
sys.stdout.flush()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.512.1",
|
|
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"
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
|
27
27
|
"conventions/",
|
|
28
|
+
"lib/",
|
|
28
29
|
"skills/",
|
|
29
30
|
"tools/",
|
|
30
31
|
"template/",
|
|
@@ -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
|
|
|
@@ -140,6 +140,22 @@ without context switching:
|
|
|
140
140
|
{列表 或 "无。"}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### Commit
|
|
144
|
+
|
|
145
|
+
扫描完成后立即提交,把 dream 发现纳入 git 历史,便于晨报追溯:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git add BACKLOG.md docs/dream/YYYY-MM-DD.md
|
|
149
|
+
# 有 REFACTOR 条目时:
|
|
150
|
+
git commit -m "chore: dream scan YYYY-MM-DD — {N} REFACTOR entries"
|
|
151
|
+
# 无发现时:
|
|
152
|
+
git commit -m "chore: dream scan YYYY-MM-DD — no findings"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- BACKLOG.md 和 dream 日志必须在**同一个 commit** 里入库,避免出现"REFACTOR 已加但日志找不到"或反过来的撕裂状态
|
|
156
|
+
- 写文件失败时不要执行 commit;保持工作区干净,由调度器负责重试
|
|
157
|
+
- 仅 `BACKLOG.md` 和 `docs/dream/YYYY-MM-DD.md` 入 commit,不要顺带带入其他无关变更
|
|
158
|
+
|
|
143
159
|
## Scheduler Configuration
|
|
144
160
|
|
|
145
161
|
roll-.dream runs **locally** — it reads the local codebase directly.
|
|
@@ -151,6 +151,19 @@ A simple heuristic — not a gate, just a signal for the human:
|
|
|
151
151
|
**Epic: {Name}** — {done}/{total} ✅
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
### Step 4.5 — Commit Brief
|
|
155
|
+
|
|
156
|
+
写完文件后立即提交,让简报进入 git 历史,便于后续追溯与跨会话审计:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
git add docs/briefs/YYYY-MM-DD-NN.md
|
|
160
|
+
git commit -m "docs: roll-brief YYYY-MM-DD-NN — {触发原因}"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- 触发原因来自调用上下文(Feature 完成 / 每日 / 手动 / `--feature` / `--since`),用一句话填入
|
|
164
|
+
- 写文件失败时不要执行 commit;保持工作区干净,由调用方处理重试
|
|
165
|
+
- 仅 `docs/briefs/` 下新文件入 commit,不要顺带带入其他无关变更
|
|
166
|
+
|
|
154
167
|
### Step 5 — Notify
|
|
155
168
|
|
|
156
169
|
写完文件后在终端或 CI 日志中打印简报路径:
|
|
@@ -50,6 +50,12 @@ $roll-design --from-plan docs/features/auth-plan.md
|
|
|
50
50
|
|
|
51
51
|
# Directly create a Story (auto-detected as User Story → Slice DDD)
|
|
52
52
|
$roll-design "user login feature"
|
|
53
|
+
|
|
54
|
+
# Non-interactive: read structured input file, skip Clarify/Discuss, write BACKLOG
|
|
55
|
+
$roll-design --from-file docs/requirements/auth-req.md
|
|
56
|
+
|
|
57
|
+
# Promote IDEA to Story: read BACKLOG IDEA-NNN, produce US-XXX, annotate IDEA
|
|
58
|
+
$roll-design --from-idea IDEA-009
|
|
53
59
|
```
|
|
54
60
|
|
|
55
61
|
## DDD Depth Scale
|
|
@@ -118,10 +124,82 @@ docs/domain/ # DDD domain model (greenfield / cross-featu
|
|
|
118
124
|
3. Plan file: `docs/features/<feature>-plan.md` (create if it doesn't exist)
|
|
119
125
|
4. BACKLOG.md index row goes under the corresponding Epic > Feature group
|
|
120
126
|
|
|
127
|
+
## Non-Interactive Mode
|
|
128
|
+
|
|
129
|
+
Activated by explicit flags or auto-detected high-confidence input. Skips Clarify and Discuss, writes stories directly to BACKLOG as `📋 Todo`, no confirm gate.
|
|
130
|
+
|
|
131
|
+
### `--from-file <path>`
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Input: structured requirement file (plain text or markdown)
|
|
135
|
+
|
|
136
|
+
Expected file contents (minimum viable):
|
|
137
|
+
- Description: what to build (1–3 sentences)
|
|
138
|
+
- Expected AC: measurable outcomes (bullet list)
|
|
139
|
+
- [Optional] Domain hint, priority, dependencies
|
|
140
|
+
|
|
141
|
+
Execution path:
|
|
142
|
+
[Read file] → [Analyze] → [DDD Slice] → [Solution Design] → [Split Stories]
|
|
143
|
+
→ [Write BACKLOG 📋 Todo] → Done (no Clarify, no Discuss, no confirm gate)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Input file example (`docs/requirements/auth-req.md`):
|
|
147
|
+
```markdown
|
|
148
|
+
## Requirement: session timeout warning
|
|
149
|
+
|
|
150
|
+
Description: Show a countdown modal 60 seconds before session expires.
|
|
151
|
+
Users can click "Stay logged in" to extend, or let it expire naturally.
|
|
152
|
+
|
|
153
|
+
Expected AC:
|
|
154
|
+
- Modal appears at T-60s with countdown timer
|
|
155
|
+
- "Stay logged in" sends a keepalive and dismisses modal
|
|
156
|
+
- Expiry after countdown logs user out and redirects to /login
|
|
157
|
+
- Works across all authenticated pages
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `--from-idea IDEA-NNN`
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Input: IDEA-NNN identifier from BACKLOG.md
|
|
164
|
+
|
|
165
|
+
Execution path:
|
|
166
|
+
[Read BACKLOG.md IDEA-NNN row] → [Analyze] → [DDD Slice] → [Split Stories]
|
|
167
|
+
→ [Write BACKLOG 📋 Todo] → [Annotate IDEA row: → US-XXX] → Done
|
|
168
|
+
|
|
169
|
+
IDEA annotation: append `→ US-XXX` to the IDEA row's Description column.
|
|
170
|
+
Example: | IDEA-009 | ... | ✅ Done → US-AUTO-021 |
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### High-Confidence Auto-Detection
|
|
174
|
+
|
|
175
|
+
When input is not a flag-based mode but already contains all three of: **clear verb**, **explicit scope**, and **verifiable acceptance signal**, skip Clarify automatically. At most retain Discuss (only if approach has genuine divergence).
|
|
176
|
+
|
|
177
|
+
High-confidence signals (all three must be present):
|
|
178
|
+
- Clear verb: add / remove / fix / rename / migrate / split / extract / support ...
|
|
179
|
+
- Explicit scope: named file, command, module, endpoint, or UI element
|
|
180
|
+
- Acceptance signal: "so that X", "when Y then Z", measurable outcome described
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
```bash
|
|
184
|
+
# High confidence — skip Clarify:
|
|
185
|
+
$roll-design "add --dry-run flag to roll loop on that prints plist without installing"
|
|
186
|
+
$roll-design "rename cmd_status() to cmd_overview() in bin/roll and update all callers"
|
|
187
|
+
|
|
188
|
+
# Low confidence — enter Clarify:
|
|
189
|
+
$roll-design "improve the status command"
|
|
190
|
+
$roll-design "make loop better"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
121
195
|
## Workflow
|
|
122
196
|
|
|
123
197
|
```
|
|
124
198
|
User Input
|
|
199
|
+
│
|
|
200
|
+
├── --from-file <path> ──→ [Read file] → Step 2 (Analyze) → Steps 3–5 → BACKLOG 📋 → Done
|
|
201
|
+
├── --from-idea IDEA-N ──→ [Read BACKLOG IDEA] → Step 2 → Steps 3–5 → BACKLOG 📋 → Annotate IDEA → Done
|
|
202
|
+
├── high-confidence str ──→ Step 2 (Analyze) directly (skip Clarify, keep Discuss if divergence)
|
|
125
203
|
│
|
|
126
204
|
▼
|
|
127
205
|
┌─────────────────────────────┐
|
|
@@ -240,9 +318,14 @@ User Input
|
|
|
240
318
|
│
|
|
241
319
|
├── Yes ──→ $roll-build US-XXX
|
|
242
320
|
│
|
|
243
|
-
└── No ──→
|
|
321
|
+
└── No ──→ Story 已写入 BACKLOG 为 📋 Todo
|
|
322
|
+
loop 下轮将自动执行
|
|
323
|
+
(选 No 仅跳过立即执行)
|
|
244
324
|
```
|
|
245
325
|
|
|
326
|
+
**Gate 语义澄清**:选 `No` 不是放弃,story 已经入 BACKLOG,下轮 loop 会自动 pickup
|
|
327
|
+
(次日 / `roll loop now`)。若想完全搁置,请显式把状态改为 🚫 Hold。
|
|
328
|
+
|
|
246
329
|
---
|
|
247
330
|
|
|
248
331
|
## DDD Output Formats
|
|
@@ -412,7 +495,11 @@ Domain: Order Context > Order Aggregate > OrderItem Entity
|
|
|
412
495
|
|
|
413
496
|
## Clarify Phase
|
|
414
497
|
|
|
415
|
-
**
|
|
498
|
+
**Skip conditions** — silently skip Clarify when any of these hold:
|
|
499
|
+
- Input uses `--from-file` or `--from-idea` flag (non-interactive mode)
|
|
500
|
+
- Input is high-confidence (clear verb + explicit scope + acceptance signal — see Non-Interactive Mode)
|
|
501
|
+
|
|
502
|
+
**Trigger conditions** — automatically enters if none of the skip conditions hold AND any of these are met:
|
|
416
503
|
- Input is a single vague sentence without clear scope
|
|
417
504
|
- Missing clear boundaries (what / who / when / where)
|
|
418
505
|
- Contains ambiguous terms like "优化一下", "改一下", "加个东西", "做个设计"
|
|
@@ -32,6 +32,11 @@ sole authority over `roll-release`.
|
|
|
32
32
|
- Any Story marked 🚫 Hold or flagged for human review
|
|
33
33
|
- Destructive operations outside normal skill scope
|
|
34
34
|
|
|
35
|
+
**Human bypass path** — roll-loop 是默认调度器,不垄断执行权。任何时刻人可直接
|
|
36
|
+
`$roll-build US-XXX` 或 `$roll-fix FIX-XXX` 绕过 loop 立即执行(紧急 bug、中断插入、
|
|
37
|
+
故事评审等场景)。loop 通过 LOCK 和 `🔨 In Progress` 状态识别并跳过人正在做的故事,
|
|
38
|
+
人机并行不会撞车(见 Concurrency Safety)。
|
|
39
|
+
|
|
35
40
|
## Configuration
|
|
36
41
|
|
|
37
42
|
```yaml
|