@seanyao/roll 2026.516.1 → 2026.517.2
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 +17 -1
- package/bin/roll +229 -90
- package/conventions/global/AGENTS.md +49 -0
- package/conventions/templates/backend-service/AGENTS.md +5 -0
- package/conventions/templates/cli/AGENTS.md +5 -0
- package/conventions/templates/frontend-only/AGENTS.md +5 -0
- package/conventions/templates/fullstack/AGENTS.md +5 -0
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +73 -1
- package/skills/roll-brief/SKILL.md +1 -1
- package/skills/roll-loop/SKILL.md +62 -6
- package/skills/roll-release/SKILL.md +0 -146
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.517.2
|
|
4
|
+
- **Fixed**: `roll dream`、`roll brief`、`roll loop` 的定时任务不再被 Claude 升级后的弹窗拦住,悄悄失效
|
|
5
|
+
|
|
6
|
+
## v2026.517.1
|
|
7
|
+
|
|
8
|
+
- **New**: loop 自动修复 story 引入的 CI 红 — 不再每次 CI 红都停下等人,修不好才写 ALERT `[loop]`
|
|
9
|
+
- **New**: Roll 官网上线 — 装、用、原理一站讲清楚
|
|
10
|
+
- **Fixed**: mac 休眠不再打断 loop cycle — 全程保持唤醒 `[loop]`
|
|
11
|
+
- **Fixed**: agent 假死时 loop 自动接管,不再无限挂起 `[loop]`
|
|
12
|
+
- **Fixed**: PR / 合并失败时 — loop 仍能把代码备份到独立分支不丢失 `[loop]`
|
|
13
|
+
- **Fixed**: loop 启动时自动恢复上一轮中断的工作,意外中断的代码不再失踪 `[loop]`
|
|
14
|
+
- **Fixed**: `roll loop now` 现在卡住状态也会先自愈再启动 `[loop]`
|
|
15
|
+
- **Fixed**: 自治 loop 不再被权限弹窗卡住 `[loop]`
|
|
16
|
+
- **Fixed**: `roll peer` 多轮 review 不再中途断线 `[peer]`
|
|
17
|
+
- **Fixed**: `roll loop runs` 现在跨子目录都能显示历史 `[loop]`
|
|
18
|
+
- **Fixed**: loop 空跑也会清理 worktree,不再随时间堆积 `[loop]`
|
|
19
|
+
|
|
3
20
|
## v2026.515.1
|
|
4
21
|
|
|
5
22
|
- **New**: `roll brief` / `roll dream` 生成文档后自动提交推送 — 每次晨报和夜检不再需要手动 commit `[loop]`
|
|
@@ -170,7 +187,6 @@ Roll 从「技能编排工具」变成了「自主执行系统」——三个新
|
|
|
170
187
|
|
|
171
188
|
## 2026.05.06
|
|
172
189
|
- **Added**: OpenCode 集成 — 检测 opencode 环境,自动同步全局 AGENTS.md 规则文件
|
|
173
|
-
- **Added**: roll-bipo-onboard 技能 — 新员工入职引导流程技能,含 bats 测试
|
|
174
190
|
- **Improved**: Git 提交归属 — 用 Co-Authored-By trailer 替代 [client] 前缀,更标准的多 AI 工具归属方式
|
|
175
191
|
- **Improved**: AGENTS.md 加入 Scope Gate — 防止技能执行时越界修改不相关文件
|
|
176
192
|
|
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.517.2"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -1493,45 +1493,19 @@ _peer_call() {
|
|
|
1493
1493
|
local out_file
|
|
1494
1494
|
out_file=$(mktemp)
|
|
1495
1495
|
local cmd_str
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
deepseek) cmd_str="deepseek $(printf %q "$prompt")" ;;
|
|
1501
|
-
codex) cmd_str="codex exec --json --output-last-message $(printf %q "$prompt")" ;;
|
|
1502
|
-
opencode) cmd_str="opencode run $(printf %q "$prompt")" ;;
|
|
1503
|
-
*)
|
|
1504
|
-
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1505
|
-
return 1 ;;
|
|
1506
|
-
esac
|
|
1496
|
+
cmd_str=$(_agent_cmd_str "$to" peer "$prompt") || {
|
|
1497
|
+
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1498
|
+
return 1
|
|
1499
|
+
}
|
|
1507
1500
|
_peer_dispatch_in_tmux "$session" "$cmd_str" "$out_file" "$stderr_log" "$call_timeout"
|
|
1508
1501
|
output="$(cat "$out_file" 2>/dev/null || true)"
|
|
1509
1502
|
rm -f "$out_file"
|
|
1510
1503
|
else
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
output="$(kimi --quiet -p "$prompt" 2>"$stderr_log" || true)"
|
|
1517
|
-
;;
|
|
1518
|
-
pi)
|
|
1519
|
-
output="$(pi -p "$prompt" 2>"$stderr_log" || true)"
|
|
1520
|
-
;;
|
|
1521
|
-
deepseek)
|
|
1522
|
-
output="$(deepseek "$prompt" 2>"$stderr_log" || true)"
|
|
1523
|
-
;;
|
|
1524
|
-
codex)
|
|
1525
|
-
output="$(codex exec --json --output-last-message "$prompt" 2>"$stderr_log" || true)"
|
|
1526
|
-
;;
|
|
1527
|
-
opencode)
|
|
1528
|
-
output="$(opencode run "$prompt" 2>"$stderr_log" || true)"
|
|
1529
|
-
;;
|
|
1530
|
-
*)
|
|
1531
|
-
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1532
|
-
return 1
|
|
1533
|
-
;;
|
|
1534
|
-
esac
|
|
1504
|
+
_agent_argv "$to" peer "$prompt" || {
|
|
1505
|
+
err "Unsupported peer: $to 不支持的 peer: $to"
|
|
1506
|
+
return 1
|
|
1507
|
+
}
|
|
1508
|
+
output="$("${_AGENT_ARGV[@]}" 2>"$stderr_log" || true)"
|
|
1535
1509
|
fi
|
|
1536
1510
|
|
|
1537
1511
|
printf '%s\n' "$output"
|
|
@@ -1884,21 +1858,71 @@ _parse_review_verdict() {
|
|
|
1884
1858
|
echo "${type}${reason:+:${reason}}"
|
|
1885
1859
|
}
|
|
1886
1860
|
|
|
1861
|
+
# REFACTOR-017: single source of truth for agent invocation argv.
|
|
1862
|
+
# Sets the global _AGENT_ARGV array to the command + args for (agent, mode, prompt).
|
|
1863
|
+
# Modes:
|
|
1864
|
+
# text — structured text (claude --output-format text; codex exec)
|
|
1865
|
+
# plain — default output (claude -p; codex exec)
|
|
1866
|
+
# peer — peer protocol (claude --output-format text; codex --json --output-last-message)
|
|
1867
|
+
# Returns 1 on unknown agent. Adding a new agent only needs an entry here.
|
|
1868
|
+
_agent_argv() {
|
|
1869
|
+
local agent="$1" mode="$2" prompt="$3"
|
|
1870
|
+
case "$agent" in
|
|
1871
|
+
claude)
|
|
1872
|
+
case "$mode" in
|
|
1873
|
+
text|peer) _AGENT_ARGV=(claude -p --output-format text "$prompt") ;;
|
|
1874
|
+
*) _AGENT_ARGV=(claude -p "$prompt") ;;
|
|
1875
|
+
esac ;;
|
|
1876
|
+
kimi) _AGENT_ARGV=(kimi --quiet -p "$prompt") ;;
|
|
1877
|
+
deepseek) _AGENT_ARGV=(deepseek "$prompt") ;;
|
|
1878
|
+
pi) _AGENT_ARGV=(pi -p "$prompt") ;;
|
|
1879
|
+
codex)
|
|
1880
|
+
case "$mode" in
|
|
1881
|
+
peer) _AGENT_ARGV=(codex exec --json --output-last-message "$prompt") ;;
|
|
1882
|
+
*) _AGENT_ARGV=(codex exec "$prompt") ;;
|
|
1883
|
+
esac ;;
|
|
1884
|
+
opencode) _AGENT_ARGV=(opencode run "$prompt") ;;
|
|
1885
|
+
*) return 1 ;;
|
|
1886
|
+
esac
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
# Build a printf %q-escaped command string for (agent, mode, prompt).
|
|
1890
|
+
# Used where the command must be passed as a string (e.g. tmux send-keys).
|
|
1891
|
+
_agent_cmd_str() {
|
|
1892
|
+
_agent_argv "$@" || return 1
|
|
1893
|
+
local i out
|
|
1894
|
+
printf -v out '%q' "${_AGENT_ARGV[0]}"
|
|
1895
|
+
for ((i = 1; i < ${#_AGENT_ARGV[@]}; i++)); do
|
|
1896
|
+
printf -v out '%s %q' "$out" "${_AGENT_ARGV[i]}"
|
|
1897
|
+
done
|
|
1898
|
+
printf '%s' "$out"
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
# Splice --dangerously-skip-permissions into _AGENT_ARGV for claude. Used by
|
|
1902
|
+
# trusted, human-triggered, or autonomous flows that should not be blocked by
|
|
1903
|
+
# Claude Code's pre-write "approve diff" UX (which silently never gets
|
|
1904
|
+
# approved in `claude -p` pipe mode). No-op for non-claude agents and for
|
|
1905
|
+
# already-bypassed argvs.
|
|
1906
|
+
_agent_bypass_claude_perms() {
|
|
1907
|
+
[[ "${_AGENT_ARGV[0]}" == "claude" ]] || return 0
|
|
1908
|
+
local arg
|
|
1909
|
+
for arg in "${_AGENT_ARGV[@]}"; do
|
|
1910
|
+
[[ "$arg" == "--dangerously-skip-permissions" ]] && return 0
|
|
1911
|
+
done
|
|
1912
|
+
_AGENT_ARGV=("${_AGENT_ARGV[@]:0:2}" --dangerously-skip-permissions "${_AGENT_ARGV[@]:2}")
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1887
1915
|
_agent_run_skill() {
|
|
1888
1916
|
local skill="$1"
|
|
1889
1917
|
local agent; agent=$(_project_agent)
|
|
1890
1918
|
local skill_file="${ROLL_HOME}/skills/${skill}/SKILL.md"
|
|
1891
1919
|
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill}"; return 1; }
|
|
1892
1920
|
local content; content=$(_skill_content "$skill_file")
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
codex) codex exec "$content" ;;
|
|
1899
|
-
opencode) opencode run "$content" ;;
|
|
1900
|
-
*) err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"; return 1 ;;
|
|
1901
|
-
esac
|
|
1921
|
+
_agent_argv "$agent" text "$content" || {
|
|
1922
|
+
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
1923
|
+
return 1
|
|
1924
|
+
}
|
|
1925
|
+
"${_AGENT_ARGV[@]}"
|
|
1902
1926
|
}
|
|
1903
1927
|
|
|
1904
1928
|
cmd_review_pr() {
|
|
@@ -1958,15 +1982,8 @@ cmd_review_pr() {
|
|
|
1958
1982
|
local agent; agent=$(_project_agent)
|
|
1959
1983
|
local output
|
|
1960
1984
|
info "Reviewing PR #${pr_number} with ${agent}..."
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
kimi) output=$(kimi --quiet -p "$prompt" 2>/dev/null) ;;
|
|
1964
|
-
deepseek) output=$(deepseek "$prompt" 2>/dev/null) ;;
|
|
1965
|
-
pi) output=$(pi -p "$prompt" 2>/dev/null) ;;
|
|
1966
|
-
codex) output=$(codex exec "$prompt" 2>/dev/null) ;;
|
|
1967
|
-
opencode) output=$(opencode run "$prompt" 2>/dev/null) ;;
|
|
1968
|
-
*) err "Unknown agent '${agent}'"; return 1 ;;
|
|
1969
|
-
esac
|
|
1985
|
+
_agent_argv "$agent" text "$prompt" || { err "Unknown agent '${agent}'"; return 1; }
|
|
1986
|
+
output=$("${_AGENT_ARGV[@]}" 2>/dev/null)
|
|
1970
1987
|
|
|
1971
1988
|
echo "$output"
|
|
1972
1989
|
|
|
@@ -2046,9 +2063,9 @@ cmd_agent() {
|
|
|
2046
2063
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2047
2064
|
|
|
2048
2065
|
_LOOP_TAG="# roll-loop"
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2066
|
+
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
2067
|
+
: "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state.yaml}"
|
|
2068
|
+
: "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT.md}"
|
|
2052
2069
|
_LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
2053
2070
|
_LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
|
|
2054
2071
|
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
@@ -2168,7 +2185,11 @@ _write_loop_runner_script() {
|
|
|
2168
2185
|
# stream-json enables realtime streaming; loop-fmt.py humanizes the events.
|
|
2169
2186
|
local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
|
|
2170
2187
|
local roll_bin="${ROLL_PKG_DIR}/bin/roll"
|
|
2171
|
-
|
|
2188
|
+
# FIX-041: loop cycle is autonomous — permission prompts and sandbox path
|
|
2189
|
+
# restrictions only cause the cycle to burn turns asking for approvals
|
|
2190
|
+
# it cannot receive. Bypass all permission checks for the inner claude
|
|
2191
|
+
# invocation. Worktree isolation contains the blast radius.
|
|
2192
|
+
local cmd_verbose="${cmd/claude -p/claude -p --verbose --dangerously-skip-permissions --output-format stream-json}"
|
|
2172
2193
|
# US-AUTO-037: strip leading `cd "<path>" && ` (callers like
|
|
2173
2194
|
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
2174
2195
|
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
@@ -2220,6 +2241,35 @@ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
|
2220
2241
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2221
2242
|
_USE_WORKTREE=0
|
|
2222
2243
|
cd "${project_path}" 2>/dev/null || true
|
|
2244
|
+
# FIX-040: orphan worktree recovery — scan for worktrees left by previous failed
|
|
2245
|
+
# cycles (publish failed or inner script was SIGKILL'd). Attempt to publish each
|
|
2246
|
+
# before starting the new cycle. Glob is chronological via timestamp in name.
|
|
2247
|
+
for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
2248
|
+
[ -d "\$_orphan_wt" ] || continue
|
|
2249
|
+
# Confirm it's a real worktree directory (not glob literal when no matches)
|
|
2250
|
+
[ -d "\${_orphan_wt}/.git" ] || [ -f "\${_orphan_wt}/.git" ] || continue
|
|
2251
|
+
_orphan_branch=\$(cd "\$_orphan_wt" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
2252
|
+
[ -z "\$_orphan_branch" ] && continue
|
|
2253
|
+
_orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
2254
|
+
if [ "\$_orphan_commits" -gt 0 ]; then
|
|
2255
|
+
echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
|
|
2256
|
+
_orphan_ok=0
|
|
2257
|
+
if ( cd "\$_orphan_wt" && _loop_is_doc_only_change ); then
|
|
2258
|
+
( cd "\$_orphan_wt" && _loop_publish_doc_pr "\$_orphan_branch" "doc: recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
2259
|
+
else
|
|
2260
|
+
( cd "\$_orphan_wt" && _loop_publish_pr "\$_orphan_branch" "recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
2261
|
+
fi
|
|
2262
|
+
if [ "\$_orphan_ok" -eq 1 ]; then
|
|
2263
|
+
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
2264
|
+
echo "[loop] FIX-040: orphan recovered and cleaned: \$_orphan_branch"
|
|
2265
|
+
else
|
|
2266
|
+
echo "[loop] FIX-040: orphan recovery publish failed for \$_orphan_branch — leaving preserved"
|
|
2267
|
+
fi
|
|
2268
|
+
else
|
|
2269
|
+
echo "[loop] FIX-040: orphan worktree \$_orphan_wt has no commits; cleaning up"
|
|
2270
|
+
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
2271
|
+
fi
|
|
2272
|
+
done
|
|
2223
2273
|
# US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
|
|
2224
2274
|
# post-claude cleanup can diff and delete only this session's additions.
|
|
2225
2275
|
CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
|
|
@@ -2229,8 +2279,12 @@ if _worktree_fetch_origin main \\
|
|
|
2229
2279
|
_worktree_submodule_init "\$WT" 2>/dev/null || true
|
|
2230
2280
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
2231
2281
|
else
|
|
2232
|
-
|
|
2233
|
-
|
|
2282
|
+
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
2283
|
+
# --dangerously-skip-permissions is only safe paired with worktree isolation;
|
|
2284
|
+
# falling back to the main tree without isolation is unacceptable.
|
|
2285
|
+
_worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
|
|
2286
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
|
|
2287
|
+
exit 0
|
|
2234
2288
|
fi
|
|
2235
2289
|
|
|
2236
2290
|
FMT="${fmt_script}"
|
|
@@ -2282,12 +2336,34 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2282
2336
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2283
2337
|
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2284
2338
|
else
|
|
2285
|
-
|
|
2286
|
-
|
|
2339
|
+
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
2340
|
+
# as final safety net so code is never local-only before worktree cleanup.
|
|
2341
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
2342
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
2343
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2344
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2345
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2346
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2347
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2348
|
+
else
|
|
2349
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
|
|
2350
|
+
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
2351
|
+
fi
|
|
2287
2352
|
fi
|
|
2288
2353
|
else
|
|
2289
|
-
|
|
2290
|
-
|
|
2354
|
+
# FIX-039: PR publish failed — push orphan branch+tag to origin as safety net.
|
|
2355
|
+
# (_loop_publish_pr may have already pushed the branch; git push is idempotent.)
|
|
2356
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
2357
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
2358
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2359
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2360
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2361
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2362
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2363
|
+
else
|
|
2364
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2365
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2366
|
+
fi
|
|
2291
2367
|
fi
|
|
2292
2368
|
fi
|
|
2293
2369
|
else
|
|
@@ -2379,7 +2455,7 @@ fi
|
|
|
2379
2455
|
echo "\$\$" > "\$LOCK"
|
|
2380
2456
|
trap 'rm -f "\$LOCK"' EXIT
|
|
2381
2457
|
if command -v tmux >/dev/null 2>&1; then
|
|
2382
|
-
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop
|
|
2458
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-${slug}\$" | while read _s; do
|
|
2383
2459
|
tmux kill-session -t "\$_s" 2>/dev/null || true
|
|
2384
2460
|
done
|
|
2385
2461
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
@@ -2520,17 +2596,24 @@ _install_launchd_plists() {
|
|
|
2520
2596
|
_agent_skill_cmd() {
|
|
2521
2597
|
local skill_path="$1"
|
|
2522
2598
|
local agent; agent=$(_project_agent)
|
|
2523
|
-
# Strip YAML frontmatter inline for cron commands
|
|
2524
2599
|
local strip="awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' '${skill_path}'"
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2600
|
+
_agent_argv "$agent" plain "__PROMPT__" || {
|
|
2601
|
+
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
2602
|
+
return 1
|
|
2603
|
+
}
|
|
2604
|
+
# Cron-installed skills (dream / brief / loop) run autonomously and need to
|
|
2605
|
+
# Edit files (docs/dream/, docs/briefs/, BACKLOG, etc.). Claude Code 2.1.x's
|
|
2606
|
+
# pre-write approval UX silently blocks `claude -p` from applying edits in
|
|
2607
|
+
# non-interactive pipe mode — bypass it for the cron context.
|
|
2608
|
+
_agent_bypass_claude_perms
|
|
2609
|
+
# In cron context, use absolute claude path so a fresh shell can find it.
|
|
2610
|
+
[[ "$agent" == "claude" ]] && _AGENT_ARGV[0]="$(command -v claude 2>/dev/null || echo claude)"
|
|
2611
|
+
# Drop the prompt sentinel (always last), re-emit head args + quoted $(strip).
|
|
2612
|
+
local out="${_AGENT_ARGV[0]}" i prompt_idx=$((${#_AGENT_ARGV[@]} - 1))
|
|
2613
|
+
for ((i = 1; i < prompt_idx; i++)); do
|
|
2614
|
+
out+=" ${_AGENT_ARGV[i]}"
|
|
2615
|
+
done
|
|
2616
|
+
echo "${out} \"\$(${strip})\""
|
|
2534
2617
|
}
|
|
2535
2618
|
|
|
2536
2619
|
cmd_loop() {
|
|
@@ -2651,15 +2734,46 @@ _loop_off() {
|
|
|
2651
2734
|
ok "Loop disabled 已停用"
|
|
2652
2735
|
}
|
|
2653
2736
|
|
|
2737
|
+
_loop_is_active() {
|
|
2738
|
+
# Three-level liveness probe used by FIX-037 heal and `roll loop now`.
|
|
2739
|
+
# Returns 0 if any signal says the cycle is alive, 1 if all signals are dead.
|
|
2740
|
+
# Heartbeat is primary (FIX-038); LOCK PID and tmux session are fallbacks.
|
|
2741
|
+
# ROLL_HEARTBEAT_TIMEOUT (default 1800s) matches the outer heredoc's threshold.
|
|
2742
|
+
local slug="${1:?slug required}"
|
|
2743
|
+
local timeout="${ROLL_HEARTBEAT_TIMEOUT:-1800}"
|
|
2744
|
+
local hb_file="${_SHARED_ROOT}/loop/.heartbeat-${slug}"
|
|
2745
|
+
if [[ -f "$hb_file" ]]; then
|
|
2746
|
+
local ts; ts=$(cat "$hb_file" 2>/dev/null || echo "")
|
|
2747
|
+
if [[ "$ts" =~ ^[0-9]+$ ]]; then
|
|
2748
|
+
local age=$(( $(date -u +%s) - ts ))
|
|
2749
|
+
[[ $age -lt $timeout ]] && return 0
|
|
2750
|
+
fi
|
|
2751
|
+
fi
|
|
2752
|
+
local lock="${_SHARED_ROOT}/loop/.LOCK-${slug}"
|
|
2753
|
+
if [[ -f "$lock" ]]; then
|
|
2754
|
+
local pid; pid=$(head -1 "$lock" 2>/dev/null || echo "")
|
|
2755
|
+
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && return 0
|
|
2756
|
+
fi
|
|
2757
|
+
command -v tmux >/dev/null 2>&1 && tmux has-session -t "roll-loop-${slug}" 2>/dev/null && return 0
|
|
2758
|
+
return 1
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2654
2761
|
_loop_now() {
|
|
2762
|
+
local project_path; project_path=$(pwd -P)
|
|
2763
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2764
|
+
# Manual `roll loop now` must not bypass FIX-037 heal: if state says running
|
|
2765
|
+
# but no live signal exists, this trigger is the canonical recovery point.
|
|
2655
2766
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
2656
|
-
|
|
2767
|
+
if _loop_is_active "$slug"; then
|
|
2768
|
+
warn "Loop already running loop 正在运行中"; return 0
|
|
2769
|
+
fi
|
|
2770
|
+
info "Stale running state detected — healing before new cycle 检测到孤儿状态,正在修复..."
|
|
2771
|
+
printf "status: idle\n" > "$_LOOP_STATE"
|
|
2772
|
+
rm -f "${_SHARED_ROOT}/loop/.LOCK-${slug}" 2>/dev/null || true
|
|
2657
2773
|
fi
|
|
2658
2774
|
# Invoke the SAME runner script that launchd would invoke — same tmux,
|
|
2659
2775
|
# same --verbose, same LOCK, same auto-attach popup. ROLL_LOOP_FORCE
|
|
2660
2776
|
# bypasses only the active-window check (manual triggers aren't time-gated).
|
|
2661
|
-
local project_path; project_path=$(pwd -P)
|
|
2662
|
-
local slug; slug=$(_project_slug "$project_path")
|
|
2663
2777
|
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
2664
2778
|
if [[ ! -f "$runner" ]]; then
|
|
2665
2779
|
err "Runner script not found: ${runner}"
|
|
@@ -2832,6 +2946,7 @@ _loop_reset() {
|
|
|
2832
2946
|
else
|
|
2833
2947
|
info "No loop state to clear 无 loop 状态可清除"
|
|
2834
2948
|
fi
|
|
2949
|
+
rm -rf "$(_loop_heal_dir)"
|
|
2835
2950
|
}
|
|
2836
2951
|
|
|
2837
2952
|
# Suppress the auto-attach popup. When the marker file exists, runner scripts
|
|
@@ -3223,6 +3338,42 @@ EOF
|
|
|
3223
3338
|
return 1
|
|
3224
3339
|
}
|
|
3225
3340
|
|
|
3341
|
+
_loop_heal_dir() {
|
|
3342
|
+
printf '%s\n' "${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/heal"
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
# Bounded CI self-heal gate. Called by the loop SKILL when the
|
|
3346
|
+
# post-build CI check goes red. Counter is per-story, persisted under
|
|
3347
|
+
# $ROLL_LOOP_DIR/heal/<story-id>.count, so retries survive cycle boundaries.
|
|
3348
|
+
#
|
|
3349
|
+
# Exit 0: another heal attempt is allowed (counter incremented). Caller should
|
|
3350
|
+
# invoke roll-fix with the failure summary, then re-run the CI gate.
|
|
3351
|
+
# Exit 1: heal disabled (ROLL_LOOP_NO_HEAL=1) or exhausted (>= ROLL_LOOP_HEAL_MAX).
|
|
3352
|
+
# Caller should write ALERT and stop.
|
|
3353
|
+
_loop_self_heal_ci() {
|
|
3354
|
+
local story_id="$1"
|
|
3355
|
+
[[ -z "$story_id" ]] && return 1
|
|
3356
|
+
[[ "${ROLL_LOOP_NO_HEAL:-0}" == "1" ]] && return 1
|
|
3357
|
+
local max="${ROLL_LOOP_HEAL_MAX:-2}"
|
|
3358
|
+
local dir; dir=$(_loop_heal_dir)
|
|
3359
|
+
local counter="${dir}/${story_id}.count"
|
|
3360
|
+
mkdir -p "$dir"
|
|
3361
|
+
local current; current=$(<"$counter") 2>/dev/null || current=0
|
|
3362
|
+
[[ "$current" =~ ^[0-9]+$ ]] || current=0
|
|
3363
|
+
[[ "$current" -ge "$max" ]] && return 1
|
|
3364
|
+
echo $((current + 1)) > "$counter"
|
|
3365
|
+
return 0
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
# Reset per-story heal counter. Called when CI eventually turns green or by
|
|
3369
|
+
# `roll loop reset`. Idempotent.
|
|
3370
|
+
_loop_clear_heal_state() {
|
|
3371
|
+
local story_id="$1"
|
|
3372
|
+
[[ -z "$story_id" ]] && return 0
|
|
3373
|
+
rm -f "$(_loop_heal_dir)/${story_id}.count" 2>/dev/null
|
|
3374
|
+
return 0
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3226
3377
|
# Verify TCR rhythm after a story completes. Returns 0 if ok, 1 if no TCR commits.
|
|
3227
3378
|
# On failure: reverts story in BACKLOG.md to 📋 Todo and writes ALERT.
|
|
3228
3379
|
_loop_enforce_tcr() {
|
|
@@ -4245,16 +4396,6 @@ p.write_text("".join(out))
|
|
|
4245
4396
|
PYEOF
|
|
4246
4397
|
}
|
|
4247
4398
|
|
|
4248
|
-
cmd_release() {
|
|
4249
|
-
local pkg; pkg=$(node -p "require('./package.json').name" 2>/dev/null || true)
|
|
4250
|
-
if [[ "$pkg" == "@seanyao/roll" ]]; then
|
|
4251
|
-
err "roll 自身发版请用 scripts/release.sh(npm publish 需要人工 2FA)"
|
|
4252
|
-
echo " Run: scripts/release.sh"
|
|
4253
|
-
exit 1
|
|
4254
|
-
fi
|
|
4255
|
-
_agent_run_skill "roll-release"
|
|
4256
|
-
}
|
|
4257
|
-
|
|
4258
4399
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4259
4400
|
# BACKLOG — show pending tasks / manage status
|
|
4260
4401
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -4951,7 +5092,6 @@ usage() {
|
|
|
4951
5092
|
echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
|
|
4952
5093
|
echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
|
|
4953
5094
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
4954
|
-
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
4955
5095
|
echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
|
|
4956
5096
|
echo " review-pr <number> [PR Review] AI-powered code review for a PR AI 代码评审"
|
|
4957
5097
|
echo ""
|
|
@@ -4983,7 +5123,6 @@ main() {
|
|
|
4983
5123
|
backlog) cmd_backlog "$@" ;;
|
|
4984
5124
|
alert) cmd_alert "$@" ;;
|
|
4985
5125
|
agent) cmd_agent "$@" ;;
|
|
4986
|
-
release) cmd_release "$@" ;;
|
|
4987
5126
|
ci) cmd_ci "$@" ;;
|
|
4988
5127
|
review-pr) cmd_review_pr "$@" ;;
|
|
4989
5128
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
@@ -50,7 +50,18 @@
|
|
|
50
50
|
`depends-on:` and `manual-only:` functional tags are allowed; `Domain:` annotation tags are not.
|
|
51
51
|
Technical details and AC go in `docs/features/`.
|
|
52
52
|
A well-written BACKLOG description can be used directly as a CHANGELOG entry.
|
|
53
|
+
- **Convention layering**: project-level convention files extend the global SOT — see §9 below.
|
|
53
54
|
- **Done**: Push + CI passes + deployed. Local-only is not done.
|
|
55
|
+
- **Post-push verification** (universal — applies to any push to main, regardless of which
|
|
56
|
+
skill drove the work):
|
|
57
|
+
- After every push, wait for the triggered CI run and verify status (`gh run watch`
|
|
58
|
+
or equivalent). Do not move on, switch tasks, or claim completion until CI is green.
|
|
59
|
+
- Before pushing any new code commit, verify the **previous** code-changing push's CI
|
|
60
|
+
is green. Never stack new code commits on top of a red CI (this is the failure
|
|
61
|
+
mode FIX-026 / `_loop_precheck_ci` exists to prevent for the loop — humans need
|
|
62
|
+
the same discipline). docs-only commits (matching CI `paths-ignore`) don't reset
|
|
63
|
+
the gate either way.
|
|
64
|
+
- If CI is red, the next action is **fix or revert**, not "queue something else".
|
|
54
65
|
- **Commit message format**:
|
|
55
66
|
- Format: `<type>: <description>` (Git Hook may auto-prepend type prefix)
|
|
56
67
|
- Types: `Story N`, `Fix`, `Refactor`, `Docs`, `Chore`
|
|
@@ -93,3 +104,41 @@ Confirm each phase clean before proceeding to the next.
|
|
|
93
104
|
- **Story details**: `docs/features/` — AC, implementation specs, dependencies
|
|
94
105
|
- **Design decisions**: `docs/domain/` — DDD models, architecture records
|
|
95
106
|
- When `docs/domain/` or `docs/features/` don't exist yet, run `$roll-doc` to bootstrap.
|
|
107
|
+
|
|
108
|
+
## 9. Convention Architecture
|
|
109
|
+
|
|
110
|
+
Roll conventions form a two-layer hierarchy. The **global** layer is the single
|
|
111
|
+
source of truth for **cross-project rules** — rules that apply regardless of
|
|
112
|
+
stack, language, or domain (e.g., BACKLOG row format, identity, TCR rhythm,
|
|
113
|
+
commit message format, scope discipline). The **project** layer carries only
|
|
114
|
+
**project-specific** rules — stack, structure, build commands, domain
|
|
115
|
+
conventions, deploy targets.
|
|
116
|
+
|
|
117
|
+
**The contract:**
|
|
118
|
+
|
|
119
|
+
1. Every project-level convention file **must declare it extends the global
|
|
120
|
+
counterpart** via a one-line foundation note at the top (e.g., "Extends
|
|
121
|
+
`~/.<agent>/AGENTS.md`" or "Extends AGENTS.md in this directory").
|
|
122
|
+
2. Project-level files **never duplicate or re-state** cross-project rules.
|
|
123
|
+
When you find yourself wanting to copy a rule down, add a pointer instead.
|
|
124
|
+
3. Cross-project rules go in `conventions/global/`. Project-specific rules go
|
|
125
|
+
in `conventions/templates/<type>/`. Anything that applies regardless of
|
|
126
|
+
stack belongs upstairs.
|
|
127
|
+
|
|
128
|
+
**Layered file pairs** — each global ↔ project pair must follow the contract:
|
|
129
|
+
|
|
130
|
+
| Global SOT | Project layer | Audience |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `conventions/global/AGENTS.md` | `conventions/templates/<type>/AGENTS.md` | All agents |
|
|
133
|
+
| `conventions/global/CLAUDE.md` | `conventions/templates/<type>/CLAUDE.md` | Claude Code |
|
|
134
|
+
| `conventions/global/GEMINI.md` | `conventions/templates/<type>/GEMINI.md` | Gemini CLI |
|
|
135
|
+
| `conventions/global/project_rules.md` | `conventions/templates/<type>/project_rules.md` | Trae IDE |
|
|
136
|
+
|
|
137
|
+
The CLAUDE / GEMINI / project_rules global files themselves declare they
|
|
138
|
+
extend this AGENTS.md, so this section's rules apply transitively to all
|
|
139
|
+
four file families.
|
|
140
|
+
|
|
141
|
+
**Why it matters**: copies drift, pointers don't. When a rule must change, it
|
|
142
|
+
changes in one place and propagates; if it's duplicated, half the copies get
|
|
143
|
+
updated and the others silently lag — which is the failure mode that produced
|
|
144
|
+
this section in the first place.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Project Conventions — Backend Service
|
|
2
2
|
|
|
3
3
|
> Reference for skills to infer backend project conventions.
|
|
4
|
+
>
|
|
5
|
+
> **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
|
|
6
|
+
> (installed by `roll setup`). For BACKLOG row format, identity, TCR
|
|
7
|
+
> rhythm, and other cross-project rules, see that file's §4. Only
|
|
8
|
+
> project-specific stack / structure / domain rules live below.
|
|
4
9
|
|
|
5
10
|
## 1. Design
|
|
6
11
|
- **API**: RESTful `/api/{res}/{id}`. Structured JSON errors.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Project Conventions — CLI Tool
|
|
2
2
|
|
|
3
3
|
> Reference for skills to infer CLI project conventions.
|
|
4
|
+
>
|
|
5
|
+
> **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
|
|
6
|
+
> (installed by `roll setup`). For BACKLOG row format, identity, TCR
|
|
7
|
+
> rhythm, and other cross-project rules, see that file's §4. Only
|
|
8
|
+
> project-specific stack / structure / domain rules live below.
|
|
4
9
|
|
|
5
10
|
## 1. Principles
|
|
6
11
|
- **Lightweight**: No server/frontend.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Project Conventions — Frontend Only
|
|
2
2
|
|
|
3
3
|
> Reference for skills to infer frontend project conventions.
|
|
4
|
+
>
|
|
5
|
+
> **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
|
|
6
|
+
> (installed by `roll setup`). For BACKLOG row format, identity, TCR
|
|
7
|
+
> rhythm, and other cross-project rules, see that file's §4. Only
|
|
8
|
+
> project-specific stack / structure / domain rules live below.
|
|
4
9
|
|
|
5
10
|
## 1. Stack
|
|
6
11
|
- **Core**: React 18+ / TS / Vite / Tailwind / shadcn/ui.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Project Conventions — Fullstack Web
|
|
2
2
|
|
|
3
3
|
> Reference for skills to infer fullstack project conventions.
|
|
4
|
+
>
|
|
5
|
+
> **Foundation**: extends the shared rules in `~/.<agent>/AGENTS.md`
|
|
6
|
+
> (installed by `roll setup`). For BACKLOG row format, identity, TCR
|
|
7
|
+
> rhythm, and other cross-project rules, see that file's §4. Only
|
|
8
|
+
> project-specific stack / structure / domain rules live below.
|
|
4
9
|
|
|
5
10
|
## 1. Stack
|
|
6
11
|
- **Frontend**: React 18+ / TS / Vite / Tailwind / shadcn/ui.
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ After successful Build & Deploy, extracts completed Stories from BACKLOG.md to g
|
|
|
19
19
|
|
|
20
20
|
- Generating commit messages or PR descriptions — this skill only runs post-deploy
|
|
21
21
|
- Recording dev diary / moments (use `$roll-notes`)
|
|
22
|
-
- Bumping package version (use
|
|
22
|
+
- Bumping package version (use the project's release script, e.g. `scripts/release.sh`)
|
|
23
23
|
|
|
24
24
|
## Workflow
|
|
25
25
|
|
|
@@ -357,3 +357,75 @@ After successful deploy in `$roll-build` / `$roll-fix`:
|
|
|
357
357
|
```
|
|
358
358
|
|
|
359
359
|
不需要 `**Added**` / `**Fixed**` 前缀,分组标题已经承担了语义分类的职责。
|
|
360
|
+
|
|
361
|
+
## 8. features.md 重写模式(产品 SOT)
|
|
362
|
+
|
|
363
|
+
US-DOC-008 — `scripts/release.sh` 在 changelog/release-notes 生成完后会再
|
|
364
|
+
调一次本 skill,请求"整体重写 `docs/features.md`"。这次调用的语义和上面
|
|
365
|
+
两种完全不同:**不是基于本版 Story 增量**,而是基于**项目整体当前状态**。
|
|
366
|
+
|
|
367
|
+
### 8.1 何时触发
|
|
368
|
+
|
|
369
|
+
release.sh 完成 changelog/release-notes 写盘后,喂一段以
|
|
370
|
+
`## 当前任务:重写 docs/features.md(Section 8)` 开头的 prompt。
|
|
371
|
+
|
|
372
|
+
### 8.2 输入
|
|
373
|
+
|
|
374
|
+
prompt 会包含:
|
|
375
|
+
- 当前 `docs/features.md`(可能为空,可能上一版本的)
|
|
376
|
+
- 当前 `BACKLOG.md` 全文(Epic / Feature 分组结构)
|
|
377
|
+
- 当前 `docs/features/` 目录清单
|
|
378
|
+
- 当前版本号
|
|
379
|
+
|
|
380
|
+
### 8.3 输出契约
|
|
381
|
+
|
|
382
|
+
把整个 `docs/features.md` 写出来。结构固定为三段:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
# Roll — Features
|
|
386
|
+
|
|
387
|
+
> 说明段(保留原文)
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## ✨ Core Highlights
|
|
392
|
+
|
|
393
|
+
- **<Feature 名>** — 1 句话产品级描述
|
|
394
|
+
- **<Feature 名>** — 1 句话产品级描述
|
|
395
|
+
- ...(3-5 条)
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Features by Epic
|
|
400
|
+
|
|
401
|
+
### <Epic 名>
|
|
402
|
+
- [<Feature 名>](docs/features/<file>.md) — 1 句话描述
|
|
403
|
+
- <Feature 名> — 1 句话描述(缺 deep doc 时不加链接)
|
|
404
|
+
|
|
405
|
+
### <Epic 名>
|
|
406
|
+
- ...
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 维护说明(保留原文)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 8.4 规则
|
|
414
|
+
|
|
415
|
+
- **Catalog 必须列出 BACKLOG 中所有 `### Feature:` 出现的 Feature 名**
|
|
416
|
+
(即使没有 deep doc 也要列)
|
|
417
|
+
- Feature 名跟 `docs/features/<file>.md` 文件名一致时,加链接到该 md
|
|
418
|
+
- 没有对应 deep doc 的 Feature,**只写 plain text 不加链接**
|
|
419
|
+
- 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
|
|
420
|
+
- 分组用 BACKLOG 的 Epic 名,原序,不重排
|
|
421
|
+
- Core Highlights 从所有 Features 里挑 3-5 个最能代表产品定位的,
|
|
422
|
+
描述用 bold 标 Feature 名后接说明;不照搬 catalog 文案
|
|
423
|
+
- **不**写 "Recent Activity" 类区块——features.md 是 SOT,全量当下状态
|
|
424
|
+
- **不**写版本号、不引用 changelog 条目
|
|
425
|
+
- 说明段(顶部 quote)和维护说明(尾部)原文保留,不要重新生成措辞
|
|
426
|
+
|
|
427
|
+
### 8.5 失败安全
|
|
428
|
+
|
|
429
|
+
如果 prompt 信息不足(BACKLOG 解析失败等),**不要部分写入** —— 输出原
|
|
430
|
+
文件内容即可。release.sh 会捕获 stdout 后比较:内容未变就不 stage。
|
|
431
|
+
|
|
@@ -17,7 +17,7 @@ description: |
|
|
|
17
17
|
> Common Sense defined in the project AGENTS.md.
|
|
18
18
|
|
|
19
19
|
Owner-facing digest of autonomous agent activity. Gives the human everything
|
|
20
|
-
needed to decide whether to
|
|
20
|
+
needed to decide whether to cut a new release — without having to read every
|
|
21
21
|
commit or diff.
|
|
22
22
|
|
|
23
23
|
## Distinct from roll-.changelog
|
|
@@ -7,7 +7,7 @@ description: |
|
|
|
7
7
|
Actions), scans BACKLOG.md for 📋 Todo items, and routes each to the
|
|
8
8
|
appropriate skill: US-XXX → $roll-build, FIX-XXX → $roll-fix,
|
|
9
9
|
REFACTOR-XXX → $roll-build. Handles agent fallback on token/network failure.
|
|
10
|
-
Never
|
|
10
|
+
Never cuts a release autonomously — release is always a human decision.
|
|
11
11
|
Triggers roll-brief when a Feature completes.
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ description: |
|
|
|
18
18
|
|
|
19
19
|
Runs on a schedule. Picks up pending BACKLOG items and executes them without
|
|
20
20
|
human intervention. The human stays informed via `roll-brief` and retains
|
|
21
|
-
sole authority over
|
|
21
|
+
sole authority over releases.
|
|
22
22
|
|
|
23
23
|
## Execution Boundary
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ sole authority over `roll-release`.
|
|
|
28
28
|
- REFACTOR-XXX (Refactors) → `$roll-build`
|
|
29
29
|
|
|
30
30
|
**What roll-loop never executes:**
|
|
31
|
-
-
|
|
31
|
+
- Releases — production deployment is always a human decision (requires 2FA in real terminal)
|
|
32
32
|
- Any Story marked 🚫 Hold or flagged for human review
|
|
33
33
|
- Destructive operations outside normal skill scope
|
|
34
34
|
|
|
@@ -37,6 +37,30 @@ sole authority over `roll-release`.
|
|
|
37
37
|
故事评审等场景)。loop 通过 LOCK 和 `🔨 In Progress` 状态识别并跳过人正在做的故事,
|
|
38
38
|
人机并行不会撞车(见 Concurrency Safety)。
|
|
39
39
|
|
|
40
|
+
## Environment Constraints (autonomous loop)
|
|
41
|
+
|
|
42
|
+
You are running inside an autonomous cycle. No human is watching this turn.
|
|
43
|
+
Adapt commands to the constraints below — otherwise you will burn turns on
|
|
44
|
+
denied operations and the cycle will idle-exit.
|
|
45
|
+
|
|
46
|
+
- **No `AskUserQuestion`**: no human can answer. If you genuinely cannot
|
|
47
|
+
proceed without a decision, write an entry to `${HOME}/.shared/roll/loop/ALERT.md`
|
|
48
|
+
describing what's needed and exit cleanly.
|
|
49
|
+
- **Avoid compound bash**: each `Bash` call must run a single command.
|
|
50
|
+
No `cmd1 && cmd2`, no `cmd1 ; cmd2`, no pipes (`|`), no `$(...)` /
|
|
51
|
+
backtick subshells, no `bash -c '...'` with nested quoting. These are
|
|
52
|
+
rejected by static analysis before they run. Chain operations as
|
|
53
|
+
separate Bash calls and read intermediate output yourself.
|
|
54
|
+
- **Prefer Read/Edit over cat/sed**: use the `Read` tool for any file
|
|
55
|
+
lookup, `Edit` for modifications. They cross sandbox boundaries that
|
|
56
|
+
`cat` / `ls` / `sed` cannot.
|
|
57
|
+
- **CWD-relative paths first**: the cycle's CWD is the per-cycle worktree.
|
|
58
|
+
Files inside it (BACKLOG.md, bin/roll, tests/, docs/) are always
|
|
59
|
+
accessible. Files at `~/.shared/roll/...` are reachable via the `Read`
|
|
60
|
+
tool but not via shell commands.
|
|
61
|
+
- **Skill invocation is the work**: route US/REFACTOR via `$roll-build`,
|
|
62
|
+
FIX via `$roll-fix`. Do not try to re-implement those flows inline.
|
|
63
|
+
|
|
40
64
|
## Configuration
|
|
41
65
|
|
|
42
66
|
```yaml
|
|
@@ -208,12 +232,44 @@ After each item completes:
|
|
|
208
232
|
it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
|
|
209
233
|
is required to work through `~/.ssh/config` host rewrites that break gh's
|
|
210
234
|
auto-detection.
|
|
211
|
-
- CI passes →
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
- CI passes → call `_loop_clear_heal_state <story_id>` (idempotent) and
|
|
236
|
+
continue normally
|
|
237
|
+
- CI fails / times out / `gh` call fails → enter **CI self-heal** (US-AUTO-041)
|
|
214
238
|
- `gh` binary not installed (`command -v gh` fails) → skip gracefully
|
|
215
239
|
(return 0). Any other `gh` error is **not** "gh unavailable" — it is a
|
|
216
240
|
hard failure and must block the gate.
|
|
241
|
+
|
|
242
|
+
**CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
shell: _loop_self_heal_ci <story_id>
|
|
246
|
+
├── exit 0 → heal attempt allowed (counter incremented)
|
|
247
|
+
│ 1. Capture failure summary:
|
|
248
|
+
│ gh run view --log-failed --repo <slug> $(gh run list --commit HEAD \
|
|
249
|
+
│ --json databaseId,conclusion -L 5 | jq -r '.[] | select(.conclusion=="failure") | .databaseId' | head -1) \
|
|
250
|
+
│ 2>/dev/null | head -200 > /tmp/roll-heal-<story_id>.log
|
|
251
|
+
│ 2. Invoke Skill("roll-fix") with brief:
|
|
252
|
+
│ "CI red after <story_id>. Failing run logs at /tmp/roll-heal-<story_id>.log.
|
|
253
|
+
│ Diagnose root cause, fix via TCR, commit, push. Do NOT change <story_id>'s
|
|
254
|
+
│ BACKLOG status — it stays ✅ Done. The fix is a follow-up."
|
|
255
|
+
│ 3. After roll-fix completes, return to step 2 (CI Gate) — re-run
|
|
256
|
+
│ `roll ci --wait`. The counter prevents infinite loops.
|
|
257
|
+
│
|
|
258
|
+
└── exit 1 → heal exhausted (>=ROLL_LOOP_HEAL_MAX, default 2) or disabled
|
|
259
|
+
(ROLL_LOOP_NO_HEAL=1):
|
|
260
|
+
1. Keep story as ✅ Done (commits are already on main — CI red is a
|
|
261
|
+
follow-up problem, not a story-failure)
|
|
262
|
+
2. Write ALERT to `~/.shared/roll/loop/ALERT.md` with:
|
|
263
|
+
- story ID, time, commit SHA
|
|
264
|
+
- heal attempts made (read counter from
|
|
265
|
+
`${ROLL_LOOP_DIR:-~/.shared/roll/loop}/heal/<story_id>.count`)
|
|
266
|
+
- last failure summary (head of /tmp/roll-heal-<story_id>.log)
|
|
267
|
+
- suggested actions: `$roll-fix` manually / inspect CI / `roll loop reset`
|
|
268
|
+
3. Skip to next story.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Bypass for debugging / cost control:** set `ROLL_LOOP_NO_HEAL=1` to restore
|
|
272
|
+
pre-US-AUTO-041 fail-fast behaviour.
|
|
217
273
|
3. Update state file: `status: idle`
|
|
218
274
|
4. Check if a Feature is now fully complete (all its Stories ✅)
|
|
219
275
|
5. If yes and `brief_on_feature_complete: true` → invoke `Skill("roll-brief")`
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: roll-release
|
|
3
|
-
license: MIT
|
|
4
|
-
allowed-tools: "Read, Edit, Bash(git:*), Bash(npm:*), Bash(sed:*), Bash(date:*), Bash(gh:*)"
|
|
5
|
-
description: "Release skill for roll maintainers. Calculates next version (YYYY.MMDD.N format, auto-increments N from today's git tags), updates VERSION in bin/roll and package.json, commits, tags, and pushes to trigger npm auto-publish via GitHub Actions. Trigger: release, publish, 发版, 发布新版本."
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Release (roll-release)
|
|
9
|
-
|
|
10
|
-
One-command publish flow for roll maintainers.
|
|
11
|
-
|
|
12
|
-
## When Not to Use
|
|
13
|
-
|
|
14
|
-
- Non-maintainer users (this skill publishes the package defined in `package.json` — confirm scope before running)
|
|
15
|
-
- Internal project releases — only for the `roll` CLI package itself
|
|
16
|
-
- Hotfixing code without a version bump (use `$roll-fix`)
|
|
17
|
-
- Generating user-facing release notes (use `$roll-.changelog`)
|
|
18
|
-
|
|
19
|
-
## Version Format
|
|
20
|
-
|
|
21
|
-
`YYYY.MMDD.N` — e.g. `2026.419.1`
|
|
22
|
-
|
|
23
|
-
- `YYYY.MMDD` = today's date, month has **no leading zero** (e.g. `420` not `0420`)
|
|
24
|
-
- `N` = auto-incremented from existing git tags for today (starts at 1)
|
|
25
|
-
|
|
26
|
-
## Execution Steps
|
|
27
|
-
|
|
28
|
-
### Step 1: Calculate Version
|
|
29
|
-
|
|
30
|
-
First, inspect recent tags to confirm the actual format in use:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
git tag | sort -V | tail -5
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then calculate:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
today=$(date +%Y.%-m%d)
|
|
40
|
-
last_n=$(git tag | grep "^v${today}\." | sed "s/^v${today}\.//" | sort -n | tail -1)
|
|
41
|
-
|
|
42
|
-
# Reuse tag if latest today's tag already points to HEAD (e.g. npm publish failed, retrying)
|
|
43
|
-
if [[ -n "$last_n" ]]; then
|
|
44
|
-
last_tag="v${today}.${last_n}"
|
|
45
|
-
if [[ "$(git rev-list -n1 "$last_tag")" == "$(git rev-parse HEAD)" ]]; then
|
|
46
|
-
version="${today}.${last_n}"
|
|
47
|
-
echo "♻️ Reusing ${last_tag} — same commit, skipping version bump"
|
|
48
|
-
# Skip Steps 2-4, jump directly to Step 5 (GitHub Release) or Step 6 (npm publish)
|
|
49
|
-
else
|
|
50
|
-
n=$(( last_n + 1 ))
|
|
51
|
-
version="${today}.${n}"
|
|
52
|
-
fi
|
|
53
|
-
else
|
|
54
|
-
version="${today}.1"
|
|
55
|
-
fi
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Show the proposed version to the user:
|
|
59
|
-
```
|
|
60
|
-
Recent tags: v2026.419.1 v2026.419.2 v2026.420.3
|
|
61
|
-
Proposed version: 2026.420.4 ← new version (code changed since last tag)
|
|
62
|
-
— or —
|
|
63
|
-
♻️ Reusing v2026.420.3 ← same commit, retry after npm failure
|
|
64
|
-
Proceed? [y/N]
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Wait for confirmation before continuing.
|
|
68
|
-
|
|
69
|
-
### Step 2: Update Version Fields
|
|
70
|
-
|
|
71
|
-
**`bin/roll`** — update the VERSION line:
|
|
72
|
-
```bash
|
|
73
|
-
sed -i '' "s/^VERSION=.*/VERSION=\"${version}\"/" bin/roll
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**`package.json`** — update the version field:
|
|
77
|
-
```bash
|
|
78
|
-
npm version "${version}" --no-git-tag-version
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Verify both files show the new version before continuing.
|
|
82
|
-
|
|
83
|
-
### Step 3: Commit
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
git add bin/roll package.json
|
|
87
|
-
git commit -m "[release] v${version}"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Step 4: Tag and Push
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
git tag "v${version}"
|
|
94
|
-
git push && git push --tags
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Step 5: Create GitHub Release
|
|
98
|
-
|
|
99
|
-
**This step is mandatory.** Without it, `roll` update notifications will not work.
|
|
100
|
-
|
|
101
|
-
Convert version to changelog date, extract notes, then create the release:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
# Convert version (2026.510.3) to changelog date (2026.05.10)
|
|
105
|
-
_year=$(echo "${version}" | cut -d. -f1)
|
|
106
|
-
_mmdd=$(echo "${version}" | cut -d. -f2)
|
|
107
|
-
if [ ${#_mmdd} -eq 3 ]; then
|
|
108
|
-
_cl_date="${_year}.0${_mmdd:0:1}.${_mmdd:1:2}"
|
|
109
|
-
else
|
|
110
|
-
_cl_date="${_year}.${_mmdd:0:2}.${_mmdd:2:2}"
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
# Extract release notes from CHANGELOG.md
|
|
114
|
-
notes=$(sed -n "/^## ${_cl_date}$/,/^## /{ /^## ${_cl_date}$/d; /^## /d; p; }" CHANGELOG.md | sed '/^[[:space:]]*$/d')
|
|
115
|
-
|
|
116
|
-
gh release create "v${version}" \
|
|
117
|
-
--title "v${version}" \
|
|
118
|
-
--notes "${notes:-Release v${version}}"
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
This enables the background update check in `bin/roll` (`_check_update_async`), which queries the GitHub Releases API.
|
|
122
|
-
|
|
123
|
-
### Step 6: Publish to npm
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
npm publish --access public
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
This will open a browser for 2FA verification. Wait for it to complete before continuing.
|
|
130
|
-
|
|
131
|
-
### Step 7: Confirm
|
|
132
|
-
|
|
133
|
-
After publish, show:
|
|
134
|
-
```
|
|
135
|
-
✅ Released v{version}
|
|
136
|
-
🏷 Tag: v{version} pushed to origin
|
|
137
|
-
📦 npm published: {package_name}@{version} # package name read from package.json
|
|
138
|
-
🐙 GitHub Release: https://github.com/{owner}/{repo}/releases/tag/v{version}
|
|
139
|
-
🔗 https://www.npmjs.com/package/{package_name}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Abort Conditions
|
|
143
|
-
|
|
144
|
-
- Uncommitted changes in `bin/roll` or `package.json` → warn and abort
|
|
145
|
-
- Tag already exists for today's N → increment N and re-propose
|
|
146
|
-
- `git push` fails → show error, do not leave a dangling local tag (run `git tag -d v{version}`)
|