@seanyao/roll 2026.517.5 → 2026.517.9
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 +42 -32
- package/bin/roll +189 -69
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +3 -0
- package/skills/roll-loop/SKILL.md +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v2026.517.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
-
|
|
34
|
-
-
|
|
3
|
+
## v2026.517.6
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`features.md` 规划中标记不再依赖 AI 自觉** — 发版脚本 AI 重写后跑机械校验自动补齐 `*(规划中)*`,规则落到 shell 里不再可能被 prompt 漂移悄悄抹掉
|
|
8
|
+
|
|
9
|
+
## v2026.517.5
|
|
10
|
+
|
|
11
|
+
合并 v2026.517.1 – v2026.517.5 全部更新。
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
|
|
15
|
+
- **`loop` CI 自愈** — story 引入的 CI 红自动修,修不好才写 ALERT,不再每次都停下等人 `[loop]`
|
|
16
|
+
- **`roll loop events`** — 查看每轮详细事件流:任务选择、评审、CI、合并全都有迹可查 `[loop]`
|
|
17
|
+
- **`features.md` 区分已上线和规划中** — 一眼看出哪些能用
|
|
18
|
+
- **七个功能区补上中英双语用户指南** `[dream]`
|
|
19
|
+
- **dream 检测功能目录过期** — 文档落后时不再悄悄无人知晓 `[dream]`
|
|
20
|
+
- **Roll 官网上线** — 装、用、原理一站讲清楚
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
|
|
24
|
+
- **`roll-build` 收尾三角度并行深审**(重用 / 质量 / 效率),自检清单新增参数膨胀、N+1 等反模式
|
|
25
|
+
- **loop 实时输出突出重点** — TCR、评审、CI gate 高亮,工具日志不再喧宾夺主 `[loop]`
|
|
26
|
+
- **loop attach 三个等待点动态反馈** — story 执行、CI、PR 合入不再像卡住 `[loop]`
|
|
27
|
+
- **官网首屏动画** — 6 秒内演示装好到自动交付的完整流程
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **多 cycle 并行不再双取同一 Todo** — 新 cycle 启动前扫 OPEN 的 loop PR 跳过已认领故事 `[loop]`
|
|
32
|
+
- **loop 等 PR 合入 main 才算交付** — 不再 CI 绿就以为代码进了主干 `[loop]`
|
|
33
|
+
- **孤儿 worktree 恢复的 PR 不再被 BEHIND 状态卡住** `[loop]`
|
|
34
|
+
- **无 PR 时 `roll ci --wait` 不再死等超时** `[loop]`
|
|
35
|
+
- **`roll loop runs` 看得到刚跑完的记录**,且跨子目录可见 `[loop]`
|
|
36
|
+
- **`roll dream` / `roll brief` / `roll loop` 定时任务不再被 Claude 升级弹窗拦住悄悄失效**
|
|
37
|
+
- **mac 休眠不再打断 loop cycle** — 全程保持唤醒 `[loop]`
|
|
38
|
+
- **agent 假死时 loop 自动接管**,不再无限挂起 `[loop]`
|
|
39
|
+
- **PR 合并失败时 loop 仍把代码备份到独立分支不丢失** `[loop]`
|
|
40
|
+
- **loop 启动时自动恢复上一轮中断工作**,意外中断的代码不再失踪 `[loop]`
|
|
41
|
+
- **`roll loop now` 卡住状态会先自愈再启动** `[loop]`
|
|
42
|
+
- **自治 loop 不再被权限弹窗卡住** `[loop]`
|
|
43
|
+
- **`roll peer` 多轮 review 不再中途断线** `[peer]`
|
|
44
|
+
- **loop 空跑也清理 worktree**,不再随时间堆积 `[loop]`
|
|
35
45
|
|
|
36
46
|
## v2026.515.1
|
|
37
47
|
|
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.517.
|
|
7
|
+
VERSION="2026.517.9"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -856,6 +856,7 @@ cmd_init() {
|
|
|
856
856
|
_merge_claude_to_project "$project_dir"
|
|
857
857
|
_write_backlog "$project_dir/BACKLOG.md"
|
|
858
858
|
_ensure_features_dir "$project_dir/docs/features"
|
|
859
|
+
_write_features_md "$project_dir/docs/features.md"
|
|
859
860
|
print_merge_summary
|
|
860
861
|
|
|
861
862
|
echo ""
|
|
@@ -970,6 +971,29 @@ _ensure_features_dir() {
|
|
|
970
971
|
_ROLL_MERGE_SUMMARY+=("created|docs/features/")
|
|
971
972
|
}
|
|
972
973
|
|
|
974
|
+
# ─── Helper: write starter docs/features.md (no-op if exists) ────────────────
|
|
975
|
+
_write_features_md() {
|
|
976
|
+
if [[ -f "$1" ]]; then
|
|
977
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|docs/features.md")
|
|
978
|
+
return
|
|
979
|
+
fi
|
|
980
|
+
mkdir -p "$(dirname "$1")"
|
|
981
|
+
cat > "$1" << 'EOF'
|
|
982
|
+
# Features
|
|
983
|
+
|
|
984
|
+
> 产品视角的功能索引。每次发版时更新,使之与 BACKLOG 保持一致。
|
|
985
|
+
> Product-level feature index. Updated at release to stay in sync with BACKLOG.
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
989
|
+
## Features by Epic
|
|
990
|
+
|
|
991
|
+
<!-- Add feature entries here as epics are completed -->
|
|
992
|
+
EOF
|
|
993
|
+
ok "Created: docs/features.md"
|
|
994
|
+
_ROLL_MERGE_SUMMARY+=("created|docs/features.md")
|
|
995
|
+
}
|
|
996
|
+
|
|
973
997
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
974
998
|
# COMMAND: status
|
|
975
999
|
# Show current state of conventions
|
|
@@ -1302,7 +1326,10 @@ _peer_dispatch_in_tmux() {
|
|
|
1302
1326
|
inner=$(mktemp /tmp/roll-peer-inner-XXXXXX.sh)
|
|
1303
1327
|
{
|
|
1304
1328
|
printf '#!/bin/bash -l\n'
|
|
1305
|
-
|
|
1329
|
+
# FIX-050: portable PATH assembly (was hardcoded /opt/homebrew/bin)
|
|
1330
|
+
printf 'for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "$HOME/.local/bin"; do\n'
|
|
1331
|
+
printf ' case ":$PATH:" in *":$_d:"*) ;; *) [ -d "$_d" ] && PATH="$_d:$PATH" ;; esac\n'
|
|
1332
|
+
printf 'done; export PATH\n'
|
|
1306
1333
|
printf '%s > %q 2> %q || true\n' "$cmd_str" "$out_file" "$err_file"
|
|
1307
1334
|
printf 'touch %q\n' "$done_file"
|
|
1308
1335
|
} > "$inner"
|
|
@@ -1842,7 +1869,8 @@ cmd_review_pr() {
|
|
|
1842
1869
|
;;
|
|
1843
1870
|
UNCERTAIN)
|
|
1844
1871
|
warn "PR #${pr_number}: UNCERTAIN — ${vreason}"
|
|
1845
|
-
|
|
1872
|
+
# FIX-052: write to per-project ALERT (was global ALERT.md).
|
|
1873
|
+
local alert_file="$_LOOP_ALERT"
|
|
1846
1874
|
mkdir -p "$(dirname "$alert_file")"
|
|
1847
1875
|
printf '[%s] PR #%s: AI review UNCERTAIN — %s\n' \
|
|
1848
1876
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr_number" "$vreason" >> "$alert_file"
|
|
@@ -1901,14 +1929,6 @@ cmd_agent() {
|
|
|
1901
1929
|
# LOOP — autonomous BACKLOG executor management
|
|
1902
1930
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1903
1931
|
|
|
1904
|
-
_LOOP_TAG="# roll-loop"
|
|
1905
|
-
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
1906
|
-
: "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state.yaml}"
|
|
1907
|
-
: "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT.md}"
|
|
1908
|
-
_LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
1909
|
-
_LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
|
|
1910
|
-
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
1911
|
-
|
|
1912
1932
|
# Returns a filesystem-safe slug combining the project basename and a 6-char
|
|
1913
1933
|
# hash of the full path, ensuring uniqueness across sibling dirs with same name.
|
|
1914
1934
|
_project_slug() {
|
|
@@ -1932,6 +1952,19 @@ _project_slug() {
|
|
|
1932
1952
|
printf '%s' "${base}-${hash}"
|
|
1933
1953
|
}
|
|
1934
1954
|
|
|
1955
|
+
_LOOP_TAG="# roll-loop"
|
|
1956
|
+
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
1957
|
+
# FIX-052: per-project loop state — ALERT/state/mute were globally shared,
|
|
1958
|
+
# causing one project's alerts to surface in another project's session and
|
|
1959
|
+
# letting concurrent cycles overwrite each other's state. Align with the
|
|
1960
|
+
# existing events-/run-/LOCK-/heartbeat-<slug> namespacing.
|
|
1961
|
+
: "${_LOOP_PROJ_SLUG:=$(_project_slug 2>/dev/null || echo default)}"
|
|
1962
|
+
: "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state-${_LOOP_PROJ_SLUG}.yaml}"
|
|
1963
|
+
: "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT-${_LOOP_PROJ_SLUG}.md}"
|
|
1964
|
+
_LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
1965
|
+
: "${_LOOP_MUTE_FILE:=${_SHARED_ROOT}/loop/mute-${_LOOP_PROJ_SLUG}}"
|
|
1966
|
+
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
1967
|
+
|
|
1935
1968
|
_config_read_int() {
|
|
1936
1969
|
local key="$1" default="$2"
|
|
1937
1970
|
local val
|
|
@@ -2003,6 +2036,28 @@ _loop_event_rotate() {
|
|
|
2003
2036
|
fi
|
|
2004
2037
|
}
|
|
2005
2038
|
|
|
2039
|
+
# FIX-050: probe brew prefix + common tool dirs to build a PATH that survives
|
|
2040
|
+
# launchd/cron's bare-env launch. Setup-time companion to the runtime
|
|
2041
|
+
# assembly snippet embedded in runner scripts.
|
|
2042
|
+
_detect_path_prepend() {
|
|
2043
|
+
local dirs=() seen="" d out=""
|
|
2044
|
+
if command -v brew >/dev/null 2>&1; then
|
|
2045
|
+
local bp; bp=$(brew --prefix 2>/dev/null || true)
|
|
2046
|
+
[[ -n "$bp" && -d "$bp/bin" ]] && dirs+=("$bp/bin")
|
|
2047
|
+
fi
|
|
2048
|
+
[[ -d /opt/homebrew/bin ]] && dirs+=("/opt/homebrew/bin")
|
|
2049
|
+
[[ -d /usr/local/bin ]] && dirs+=("/usr/local/bin")
|
|
2050
|
+
[[ -d /opt/local/bin ]] && dirs+=("/opt/local/bin")
|
|
2051
|
+
[[ -d "$HOME/.local/bin" ]] && dirs+=("$HOME/.local/bin")
|
|
2052
|
+
dirs+=("/usr/bin" "/bin" "/usr/sbin" "/sbin")
|
|
2053
|
+
for d in "${dirs[@]}"; do
|
|
2054
|
+
case ":$seen:" in *":$d:"*) continue ;; esac
|
|
2055
|
+
seen="$seen:$d"
|
|
2056
|
+
[[ -z "$out" ]] && out="$d" || out="$out:$d"
|
|
2057
|
+
done
|
|
2058
|
+
printf '%s' "$out"
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2006
2061
|
_launchd_label() {
|
|
2007
2062
|
local service="$1" project_path="$2"
|
|
2008
2063
|
printf 'com.roll.%s.%s' "$service" "$(_project_slug "$project_path")"
|
|
@@ -2022,6 +2077,11 @@ _write_launchd_plist() {
|
|
|
2022
2077
|
<integer>${hour}</integer>
|
|
2023
2078
|
"
|
|
2024
2079
|
|
|
2080
|
+
# FIX-050: bake PATH into the plist so launchd-spawned bash can find tmux,
|
|
2081
|
+
# claude, node, etc. The runner script also re-asserts PATH at runtime as
|
|
2082
|
+
# a second layer (covers stale plists where brew was installed after setup).
|
|
2083
|
+
local path_value; path_value=$(_detect_path_prepend)
|
|
2084
|
+
|
|
2025
2085
|
local content
|
|
2026
2086
|
content="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
|
2027
2087
|
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
|
@@ -2035,6 +2095,11 @@ _write_launchd_plist() {
|
|
|
2035
2095
|
<string>-l</string>
|
|
2036
2096
|
<string>${runner_script}</string>
|
|
2037
2097
|
</array>
|
|
2098
|
+
<key>EnvironmentVariables</key>
|
|
2099
|
+
<dict>
|
|
2100
|
+
<key>PATH</key>
|
|
2101
|
+
<string>${path_value}</string>
|
|
2102
|
+
</dict>
|
|
2038
2103
|
<key>StartCalendarInterval</key>
|
|
2039
2104
|
<dict>
|
|
2040
2105
|
<key>Minute</key>
|
|
@@ -2093,7 +2158,13 @@ _write_loop_runner_script() {
|
|
|
2093
2158
|
cat > "$inner_path" << INNER
|
|
2094
2159
|
#!/bin/bash -l
|
|
2095
2160
|
set -o pipefail
|
|
2096
|
-
|
|
2161
|
+
# FIX-050: portable PATH assembly — launchd/cron deliver a bare PATH that
|
|
2162
|
+
# misses brew-installed tools (tmux, claude, node, …). Iterate candidate
|
|
2163
|
+
# dirs; only prepend when present and not already in PATH. Idempotent.
|
|
2164
|
+
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin"; do
|
|
2165
|
+
case ":\$PATH:" in *":\$_d:"*) ;; *) [ -d "\$_d" ] && PATH="\$_d:\$PATH" ;; esac
|
|
2166
|
+
done
|
|
2167
|
+
export PATH
|
|
2097
2168
|
# FIX-031: inner-level LOCK (PID + start-ts) — outer runner.sh LOCK can be
|
|
2098
2169
|
# bypassed (recovery / retry / direct invocation); this guards the actual
|
|
2099
2170
|
# claude invocation so a second session can't run under the same project.
|
|
@@ -2128,6 +2199,15 @@ trap 'kill "\${_HEARTBEAT_PID}" 2>/dev/null; rm -f "\$INNER_LOCK" "\$HEARTBEAT_F
|
|
|
2128
2199
|
source "${roll_bin}"
|
|
2129
2200
|
set +e
|
|
2130
2201
|
|
|
2202
|
+
# FIX-052: bin/roll initializes loop state paths from cwd at source time, but
|
|
2203
|
+
# the inner script may be launched from anywhere. Override to this project's
|
|
2204
|
+
# slug (baked at template generation) so helpers like _worktree_alert write
|
|
2205
|
+
# to the correct project's ALERT-<slug>.md / state-<slug>.yaml / mute-<slug>.
|
|
2206
|
+
_LOOP_PROJ_SLUG="${slug}"
|
|
2207
|
+
_LOOP_ALERT="\${_SHARED_ROOT}/loop/ALERT-${slug}.md"
|
|
2208
|
+
_LOOP_STATE="\${_SHARED_ROOT}/loop/state-${slug}.yaml"
|
|
2209
|
+
_LOOP_MUTE_FILE="\${_SHARED_ROOT}/loop/mute-${slug}"
|
|
2210
|
+
|
|
2131
2211
|
# Pre-claude: try to create a per-cycle isolated worktree on origin/main.
|
|
2132
2212
|
# On any failure (no remote, no main, etc.) fall back to running in the
|
|
2133
2213
|
# project's main tree (degraded — no isolation, like pre-037 behavior).
|
|
@@ -2336,6 +2416,13 @@ INNER
|
|
|
2336
2416
|
|
|
2337
2417
|
cat > "$script_path" << SCRIPT
|
|
2338
2418
|
#!/bin/bash -l
|
|
2419
|
+
# FIX-050: portable PATH assembly before any brew-tool lookup (tmux, caffeinate
|
|
2420
|
+
# on some systems, claude). Mirrors the inner script's bootstrap so even when
|
|
2421
|
+
# launchd's plist EnvironmentVariables is stale, the runner self-repairs.
|
|
2422
|
+
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin"; do
|
|
2423
|
+
case ":\$PATH:" in *":\$_d:"*) ;; *) [ -d "\$_d" ] && PATH="\$_d:\$PATH" ;; esac
|
|
2424
|
+
done
|
|
2425
|
+
export PATH
|
|
2339
2426
|
# caffeinate: prevent idle sleep from killing claude during cycles
|
|
2340
2427
|
caffeinate -i -w \$\$ &
|
|
2341
2428
|
# Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
|
|
@@ -2353,7 +2440,9 @@ if [ -z "\$ROLL_LOOP_FORCE" ] && [ -f "\$PAUSE" ]; then exit 0; fi
|
|
|
2353
2440
|
# FIX-038: heartbeat is the primary liveness signal (avoids PID reuse race);
|
|
2354
2441
|
# LOCK pid check is secondary fallback for backward compatibility.
|
|
2355
2442
|
HEARTBEAT_TIMEOUT="\${ROLL_HEARTBEAT_TIMEOUT:-1800}"
|
|
2356
|
-
STATE_FILE
|
|
2443
|
+
# FIX-052: per-project STATE_FILE (was global state.yaml — caused two projects
|
|
2444
|
+
# to clobber each other's cycle state).
|
|
2445
|
+
STATE_FILE="${HOME}/.shared/roll/loop/state-${slug}.yaml"
|
|
2357
2446
|
if [ -f "\$STATE_FILE" ]; then
|
|
2358
2447
|
_state=\$(grep '^status:' "\$STATE_FILE" | awk '{print \$2}' 2>/dev/null || echo "")
|
|
2359
2448
|
if [ "\$_state" = "running" ]; then
|
|
@@ -2384,7 +2473,9 @@ if [ -f "\$STATE_FILE" ]; then
|
|
|
2384
2473
|
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] FIX-037: orphan state detected (status=running, heartbeat stale or missing) — healing to idle" >> "\$LOG"
|
|
2385
2474
|
echo "status: idle" > "\${STATE_FILE}.tmp" && mv "\${STATE_FILE}.tmp" "\$STATE_FILE"
|
|
2386
2475
|
rm -f "\$_lock_file" 2>/dev/null || true
|
|
2387
|
-
|
|
2476
|
+
# FIX-052: per-project ALERT file (was shared ALERT.md — caused R0
|
|
2477
|
+
# auto-heal entries to surface in Roll's alert view).
|
|
2478
|
+
_alert_file="\$(dirname "\$0")/ALERT-${slug}.md"
|
|
2388
2479
|
echo "\$(date '+%Y-%m-%dT%H:%M:%S%z') | FIX-037 auto-heal | Orphan state detected and cleared (status=running → idle)" >> "\$_alert_file" 2>/dev/null || true
|
|
2389
2480
|
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] FIX-037: healed to idle, ALERT written" >> "\$LOG"
|
|
2390
2481
|
fi
|
|
@@ -2420,7 +2511,7 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
2420
2511
|
# Auto-attach popup: when not muted, spawn a Terminal window attached to the
|
|
2421
2512
|
# tmux session so the user can watch the loop work in real time. Best-effort
|
|
2422
2513
|
# focus retention: capture the current frontmost app and re-activate after.
|
|
2423
|
-
if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2514
|
+
if [ ! -f "\$HOME/.shared/roll/loop/mute-${slug}" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
2424
2515
|
# Runtime terminal detection: try preferred first, fallback through installed apps.
|
|
2425
2516
|
# open -na returns non-zero when app not found, so || chain works as fallback.
|
|
2426
2517
|
_launched=0
|
|
@@ -2515,7 +2606,8 @@ _install_launchd_plists() {
|
|
|
2515
2606
|
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
2516
2607
|
local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
|
|
2517
2608
|
local runner="${shared}/${svc}/run-${slug}.sh"
|
|
2518
|
-
|
|
2609
|
+
# FIX-052: per-project cron log so concurrent projects don't interleave.
|
|
2610
|
+
local log="${shared}/${svc}/cron-${slug}.log"
|
|
2519
2611
|
local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
|
|
2520
2612
|
|
|
2521
2613
|
if [[ "$svc" == "loop" ]]; then
|
|
@@ -2643,10 +2735,12 @@ _loop_on() {
|
|
|
2643
2735
|
|
|
2644
2736
|
mkdir -p "${_SHARED_ROOT}/loop" "${_SHARED_ROOT}/dream" "${_SHARED_ROOT}/brief"
|
|
2645
2737
|
|
|
2738
|
+
# FIX-052: per-project cron logs so concurrent projects don't interleave.
|
|
2739
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2646
2740
|
local loop_cmd dream_cmd brief_cmd
|
|
2647
|
-
loop_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-loop/SKILL.md") >> ${_SHARED_ROOT}/loop/cron.log 2>&1"
|
|
2648
|
-
dream_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-.dream/SKILL.md") >> ${_SHARED_ROOT}/dream/cron.log 2>&1"
|
|
2649
|
-
brief_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-brief/SKILL.md") >> ${_SHARED_ROOT}/brief/cron.log 2>&1"
|
|
2741
|
+
loop_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-loop/SKILL.md") >> ${_SHARED_ROOT}/loop/cron-${slug}.log 2>&1"
|
|
2742
|
+
dream_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-.dream/SKILL.md") >> ${_SHARED_ROOT}/dream/cron-${slug}.log 2>&1"
|
|
2743
|
+
brief_cmd="cd \"${project_path}\" && $(_agent_skill_cmd "${sd}/roll-brief/SKILL.md") >> ${_SHARED_ROOT}/brief/cron-${slug}.log 2>&1"
|
|
2650
2744
|
|
|
2651
2745
|
(
|
|
2652
2746
|
crontab -l 2>/dev/null
|
|
@@ -2757,7 +2851,8 @@ _loop_test() {
|
|
|
2757
2851
|
err "Run 'roll loop on' first to generate it."
|
|
2758
2852
|
return 1
|
|
2759
2853
|
fi
|
|
2760
|
-
|
|
2854
|
+
# FIX-052: per-project log so test cycle output doesn't mix with other projects'.
|
|
2855
|
+
local log="${_SHARED_ROOT}/loop/cron-${slug}.log"
|
|
2761
2856
|
local test_runner="${_SHARED_ROOT}/loop/run-${slug}-test.sh"
|
|
2762
2857
|
|
|
2763
2858
|
# Detect terminal pref same way _install_launchd_plists does
|
|
@@ -3343,7 +3438,7 @@ _loop_self_heal_ci() {
|
|
|
3343
3438
|
[[ -z "$story_id" ]] && return 1
|
|
3344
3439
|
[[ "${ROLL_LOOP_NO_HEAL:-0}" == "1" ]] && return 1
|
|
3345
3440
|
local max="${ROLL_LOOP_HEAL_MAX:-2}"
|
|
3346
|
-
local state="$
|
|
3441
|
+
local state="$_LOOP_STATE"
|
|
3347
3442
|
local current=0
|
|
3348
3443
|
if [[ -f "$state" ]]; then
|
|
3349
3444
|
local raw; raw=$(grep '^heal_count:' "$state" 2>/dev/null | awk '{print $2}')
|
|
@@ -3367,7 +3462,7 @@ _loop_self_heal_ci() {
|
|
|
3367
3462
|
_loop_clear_heal_state() {
|
|
3368
3463
|
local story_id="$1"
|
|
3369
3464
|
[[ -z "$story_id" ]] && return 0
|
|
3370
|
-
local state="$
|
|
3465
|
+
local state="$_LOOP_STATE"
|
|
3371
3466
|
[[ ! -f "$state" ]] && return 0
|
|
3372
3467
|
local tmp; tmp=$(mktemp)
|
|
3373
3468
|
grep -v '^heal_count:' "$state" > "$tmp"
|
|
@@ -3533,7 +3628,7 @@ _loop_pr_rebase_circuit() {
|
|
|
3533
3628
|
local pr="$1"
|
|
3534
3629
|
[ -n "$pr" ] || return 1
|
|
3535
3630
|
|
|
3536
|
-
local state="$
|
|
3631
|
+
local state="$_LOOP_STATE"
|
|
3537
3632
|
local now; now=$(date -u +%s)
|
|
3538
3633
|
local cutoff=$((now - 86400))
|
|
3539
3634
|
|
|
@@ -3651,7 +3746,7 @@ _loop_pr_rebase_stale() {
|
|
|
3651
3746
|
local is_fork
|
|
3652
3747
|
is_fork=$(echo "$pr_json" | jq -r '.isCrossRepository // false' 2>/dev/null)
|
|
3653
3748
|
if [ "$is_fork" = "true" ]; then
|
|
3654
|
-
local alert="$
|
|
3749
|
+
local alert="$_LOOP_ALERT"
|
|
3655
3750
|
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3656
3751
|
printf '[%s] PR #%s: fork PR — cannot rebase (no write access)\n' \
|
|
3657
3752
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" >> "$alert"
|
|
@@ -3666,7 +3761,7 @@ _loop_pr_rebase_stale() {
|
|
|
3666
3761
|
else
|
|
3667
3762
|
git rebase --abort 2>/dev/null || true
|
|
3668
3763
|
git checkout - 2>/dev/null || true
|
|
3669
|
-
local alert="$
|
|
3764
|
+
local alert="$_LOOP_ALERT"
|
|
3670
3765
|
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3671
3766
|
printf '[%s] PR #%s: rebase conflict on %s — please rebase manually\n' \
|
|
3672
3767
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$pr" "$head_ref" >> "$alert"
|
|
@@ -3717,9 +3812,17 @@ _loop_pr_inbox() {
|
|
|
3717
3812
|
|
|
3718
3813
|
# Bot review gate: if a GHA workflow already handled this PR, defer to it.
|
|
3719
3814
|
if [ "$bot_review" = "APPROVED" ]; then
|
|
3815
|
+
# All gates cleared (bot-approved + CI green + no conflicts) → merge directly.
|
|
3816
|
+
# Relying on repo-level auto-merge being configured is not reliable; loop
|
|
3817
|
+
# owns the decision here since it already ran the review.
|
|
3818
|
+
if [ "$ci_state" = "success" ] && [ "$mergeable" = "MERGEABLE" ]; then
|
|
3819
|
+
gh -R "$slug" pr merge "$num" --squash --delete-branch >/dev/null 2>&1 \
|
|
3820
|
+
&& info "PR #${num}: bot-approved + CI green — merged" \
|
|
3821
|
+
|| warn "PR #${num}: merge failed (bot-approved + CI green) — left open"
|
|
3822
|
+
fi
|
|
3720
3823
|
i=$((i + 1)); continue
|
|
3721
3824
|
elif [ "$bot_review" = "CHANGES_REQUESTED" ]; then
|
|
3722
|
-
local alert="$
|
|
3825
|
+
local alert="$_LOOP_ALERT"
|
|
3723
3826
|
mkdir -p "$(dirname "$alert")" 2>/dev/null || true
|
|
3724
3827
|
printf '[%s] PR #%s: bot review CHANGES_REQUESTED — loop PR rejected by GHA reviewer\n' \
|
|
3725
3828
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$num" >> "$alert"
|
|
@@ -3747,6 +3850,45 @@ _loop_pr_inbox() {
|
|
|
3747
3850
|
return 0
|
|
3748
3851
|
}
|
|
3749
3852
|
|
|
3853
|
+
# FIX-048: report story IDs already claimed by open loop/* PRs so a new cycle
|
|
3854
|
+
# can skip them before scanning BACKLOG. Without this gate, a cycle launched
|
|
3855
|
+
# before the previous cycle's PR merges would re-pick the same Todo story
|
|
3856
|
+
# (its worktree branches from main, where the 🔨 mark is not yet visible).
|
|
3857
|
+
#
|
|
3858
|
+
# _loop_pr_claimed_stories
|
|
3859
|
+
# Stdout: one story ID per line, deduped. Empty when nothing claimed.
|
|
3860
|
+
# Exit: 0 always (lenient: gh missing / API failure → empty output).
|
|
3861
|
+
_loop_pr_claimed_stories() {
|
|
3862
|
+
local slug; _gh_resolve slug || return 0
|
|
3863
|
+
local branches
|
|
3864
|
+
branches=$(gh -R "$slug" pr list --state open \
|
|
3865
|
+
--json headRefName \
|
|
3866
|
+
--jq '.[] | select(.headRefName | startswith("loop/")) | .headRefName' \
|
|
3867
|
+
2>/dev/null) || return 0
|
|
3868
|
+
[ -n "$branches" ] || return 0
|
|
3869
|
+
|
|
3870
|
+
local branch claimed=""
|
|
3871
|
+
while IFS= read -r branch; do
|
|
3872
|
+
[ -n "$branch" ] || continue
|
|
3873
|
+
local content
|
|
3874
|
+
content=$(gh -R "$slug" api \
|
|
3875
|
+
"repos/${slug}/contents/BACKLOG.md?ref=${branch}" \
|
|
3876
|
+
-H "Accept: application/vnd.github.raw" 2>/dev/null) || continue
|
|
3877
|
+
[ -n "$content" ] || continue
|
|
3878
|
+
local ids
|
|
3879
|
+
ids=$(printf '%s\n' "$content" \
|
|
3880
|
+
| awk -F'|' '/🔨 In Progress/ {
|
|
3881
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
|
|
3882
|
+
sub(/^\[/, "", $2)
|
|
3883
|
+
sub(/\].*$/, "", $2)
|
|
3884
|
+
if ($2 != "") print $2
|
|
3885
|
+
}')
|
|
3886
|
+
[ -n "$ids" ] && claimed="${claimed}${ids}"$'\n'
|
|
3887
|
+
done <<< "$branches"
|
|
3888
|
+
|
|
3889
|
+
printf '%s' "$claimed" | awk 'NF' | sort -u
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3750
3892
|
# US-CL-004: changelog 风格守门 Phase 1 — mechanical linter.
|
|
3751
3893
|
#
|
|
3752
3894
|
# _changelog_lint_bullet <bullet-text>
|
|
@@ -4495,6 +4637,23 @@ _backlog_extract_id() {
|
|
|
4495
4637
|
fi
|
|
4496
4638
|
}
|
|
4497
4639
|
|
|
4640
|
+
# Render one pending-group section (FIX / US / REFACTOR / IDEA) — all four
|
|
4641
|
+
# types share identical row structure, so they share one render path. Format
|
|
4642
|
+
# changes only need to happen here.
|
|
4643
|
+
# $1 title (EN + ZH) $2 ANSI color $3 count $4 id column width $5 items text
|
|
4644
|
+
_backlog_render_group() {
|
|
4645
|
+
local title="$1" color="$2" count="$3" width="$4" items="$5"
|
|
4646
|
+
echo -e " ${color}${title} (${count})${NC}"
|
|
4647
|
+
while IFS= read -r line; do
|
|
4648
|
+
[[ -z "$line" ]] && continue
|
|
4649
|
+
local id desc
|
|
4650
|
+
id=$(_backlog_extract_id "$line")
|
|
4651
|
+
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
4652
|
+
printf " %-${width}s %s\n" "$id" "$desc"
|
|
4653
|
+
done <<< "$items"
|
|
4654
|
+
echo ""
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4498
4657
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4499
4658
|
# CI — check or wait for current commit's CI status
|
|
4500
4659
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -4650,49 +4809,10 @@ cmd_backlog() {
|
|
|
4650
4809
|
echo -e " ${BOLD}Pending Backlog 待处理任务${NC} (${total} items)"
|
|
4651
4810
|
echo ""
|
|
4652
4811
|
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
id=$(echo "$line" | awk -F'|' '{print $2}' | tr -d ' ')
|
|
4658
|
-
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
4659
|
-
printf " %-12s %s\n" "$id" "$desc"
|
|
4660
|
-
done <<< "$fix_items"
|
|
4661
|
-
echo ""
|
|
4662
|
-
fi
|
|
4663
|
-
|
|
4664
|
-
if [[ $us_count -gt 0 ]]; then
|
|
4665
|
-
echo -e " ${CYAN}User Stories 用户故事 (${us_count})${NC}"
|
|
4666
|
-
while IFS= read -r line; do
|
|
4667
|
-
local id desc
|
|
4668
|
-
id=$(echo "$line" | sed 's/.*\[\(US-[^]]*\)\].*/\1/')
|
|
4669
|
-
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
4670
|
-
printf " %-14s %s\n" "$id" "$desc"
|
|
4671
|
-
done <<< "$us_items"
|
|
4672
|
-
echo ""
|
|
4673
|
-
fi
|
|
4674
|
-
|
|
4675
|
-
if [[ $refactor_count -gt 0 ]]; then
|
|
4676
|
-
echo -e " ${YELLOW}Refactors 重构 (${refactor_count})${NC}"
|
|
4677
|
-
while IFS= read -r line; do
|
|
4678
|
-
local id desc
|
|
4679
|
-
id=$(echo "$line" | awk -F'|' '{print $2}' | tr -d ' ')
|
|
4680
|
-
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
4681
|
-
printf " %-16s %s\n" "$id" "$desc"
|
|
4682
|
-
done <<< "$refactor_items"
|
|
4683
|
-
echo ""
|
|
4684
|
-
fi
|
|
4685
|
-
|
|
4686
|
-
if [[ $idea_count -gt 0 ]]; then
|
|
4687
|
-
echo -e " ${NC}Ideas 创意 (${idea_count})"
|
|
4688
|
-
while IFS= read -r line; do
|
|
4689
|
-
local id desc
|
|
4690
|
-
id=$(echo "$line" | awk -F'|' '{print $2}' | tr -d ' ')
|
|
4691
|
-
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
4692
|
-
printf " %-14s %s\n" "$id" "$desc"
|
|
4693
|
-
done <<< "$idea_items"
|
|
4694
|
-
echo ""
|
|
4695
|
-
fi
|
|
4812
|
+
[[ $fix_count -gt 0 ]] && _backlog_render_group "Bug Fixes 缺陷修复" "$RED" "$fix_count" 12 "$fix_items"
|
|
4813
|
+
[[ $us_count -gt 0 ]] && _backlog_render_group "User Stories 用户故事" "$CYAN" "$us_count" 14 "$us_items"
|
|
4814
|
+
[[ $refactor_count -gt 0 ]] && _backlog_render_group "Refactors 重构" "$YELLOW" "$refactor_count" 16 "$refactor_items"
|
|
4815
|
+
[[ $idea_count -gt 0 ]] && _backlog_render_group "Ideas 创意" "$NC" "$idea_count" 14 "$idea_items"
|
|
4696
4816
|
|
|
4697
4817
|
if [[ $total -eq 0 ]]; then
|
|
4698
4818
|
echo -e " ${GREEN}✓ Nothing pending — backlog is clear 暂无待处理任务${NC}"
|
package/package.json
CHANGED
|
@@ -420,6 +420,9 @@ prompt 会包含:
|
|
|
420
420
|
- 该 Feature 下**所有** Story 均为 `📋 Todo` → 在描述末尾追加 `*(规划中)*`
|
|
421
421
|
- 只要有 **≥1 个** `✅ Done` Story → 正常展示,**不加**任何标记
|
|
422
422
|
- 一眼可见:规划中的 Feature 在每个 Epic 分组的末尾列出
|
|
423
|
+
- **FIX-051 兜底**:`scripts/release.sh` 在 AI 重写后会跑机械校验
|
|
424
|
+
`_enforce_planning_markers`,即使本规则被 AI 漏掉也会自动补 `*(规划中)*`;
|
|
425
|
+
规则的权威实现是 release.sh 里的纯 shell 函数,prompt 这条只是软提示
|
|
423
426
|
- 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
|
|
424
427
|
- 分组用 BACKLOG 的 Epic 名,原序,不重排
|
|
425
428
|
- Core Highlights 从所有 Features 里挑 3-5 个最能代表产品定位的,
|
|
@@ -146,6 +146,19 @@ Priority: FIX-XXX first (bugs block progress), then US-XXX, then REFACTOR-XXX.
|
|
|
146
146
|
- An earlier loop iteration that hasn't finished yet (rare; should be guarded by LOCK)
|
|
147
147
|
- A previous interrupted run (the resume logic in Step 1 will pick these up)
|
|
148
148
|
|
|
149
|
+
**In-flight PR gate** (FIX-048). Before picking, also exclude stories already
|
|
150
|
+
claimed by an **open `loop/*` PR**. Each cycle's worktree is branched from
|
|
151
|
+
`origin/main`, so a story another cycle has marked 🔨 In Progress is invisible
|
|
152
|
+
locally until that cycle's PR merges. Without this gate, two cycles started
|
|
153
|
+
back-to-back will both pick the same Todo row and produce duplicate PRs.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bash -c 'source "$(command -v roll)"; _loop_pr_claimed_stories'
|
|
157
|
+
# stdout: one story ID per line (deduped) — these are claimed by open
|
|
158
|
+
# loop/* PRs on the remote. SKIP any candidate whose ID appears.
|
|
159
|
+
# exit 0 always (lenient: gh missing / API error → empty output).
|
|
160
|
+
```
|
|
161
|
+
|
|
149
162
|
**Dependency gate** (FIX-032). For each `📋 Todo` candidate, before picking:
|
|
150
163
|
|
|
151
164
|
```bash
|