@seanyao/roll 2026.516.1 → 2026.517.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 +4 -1
- package/bin/roll +210 -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,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.517.1
|
|
4
|
+
|
|
5
|
+
- **New**: loop 自动修复 story 引入的 CI 红 — 不再每次 CI 红都停下等人,修不好才写 ALERT `[loop]`
|
|
6
|
+
|
|
3
7
|
## v2026.515.1
|
|
4
8
|
|
|
5
9
|
- **New**: `roll brief` / `roll dream` 生成文档后自动提交推送 — 每次晨报和夜检不再需要手动 commit `[loop]`
|
|
@@ -170,7 +174,6 @@ Roll 从「技能编排工具」变成了「自主执行系统」——三个新
|
|
|
170
174
|
|
|
171
175
|
## 2026.05.06
|
|
172
176
|
- **Added**: OpenCode 集成 — 检测 opencode 环境,自动同步全局 AGENTS.md 规则文件
|
|
173
|
-
- **Added**: roll-bipo-onboard 技能 — 新员工入职引导流程技能,含 bats 测试
|
|
174
177
|
- **Improved**: Git 提交归属 — 用 Co-Authored-By trailer 替代 [client] 前缀,更标准的多 AI 工具归属方式
|
|
175
178
|
- **Improved**: AGENTS.md 加入 Scope Gate — 防止技能执行时越界修改不相关文件
|
|
176
179
|
|
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.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"
|
|
@@ -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,57 @@ _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
|
+
|
|
1887
1901
|
_agent_run_skill() {
|
|
1888
1902
|
local skill="$1"
|
|
1889
1903
|
local agent; agent=$(_project_agent)
|
|
1890
1904
|
local skill_file="${ROLL_HOME}/skills/${skill}/SKILL.md"
|
|
1891
1905
|
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill}"; return 1; }
|
|
1892
1906
|
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
|
|
1907
|
+
_agent_argv "$agent" text "$content" || {
|
|
1908
|
+
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
1909
|
+
return 1
|
|
1910
|
+
}
|
|
1911
|
+
"${_AGENT_ARGV[@]}"
|
|
1902
1912
|
}
|
|
1903
1913
|
|
|
1904
1914
|
cmd_review_pr() {
|
|
@@ -1958,15 +1968,8 @@ cmd_review_pr() {
|
|
|
1958
1968
|
local agent; agent=$(_project_agent)
|
|
1959
1969
|
local output
|
|
1960
1970
|
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
|
|
1971
|
+
_agent_argv "$agent" text "$prompt" || { err "Unknown agent '${agent}'"; return 1; }
|
|
1972
|
+
output=$("${_AGENT_ARGV[@]}" 2>/dev/null)
|
|
1970
1973
|
|
|
1971
1974
|
echo "$output"
|
|
1972
1975
|
|
|
@@ -2046,9 +2049,9 @@ cmd_agent() {
|
|
|
2046
2049
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2047
2050
|
|
|
2048
2051
|
_LOOP_TAG="# roll-loop"
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
+
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
2053
|
+
: "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state.yaml}"
|
|
2054
|
+
: "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT.md}"
|
|
2052
2055
|
_LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
2053
2056
|
_LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
|
|
2054
2057
|
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
@@ -2168,7 +2171,11 @@ _write_loop_runner_script() {
|
|
|
2168
2171
|
# stream-json enables realtime streaming; loop-fmt.py humanizes the events.
|
|
2169
2172
|
local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
|
|
2170
2173
|
local roll_bin="${ROLL_PKG_DIR}/bin/roll"
|
|
2171
|
-
|
|
2174
|
+
# FIX-041: loop cycle is autonomous — permission prompts and sandbox path
|
|
2175
|
+
# restrictions only cause the cycle to burn turns asking for approvals
|
|
2176
|
+
# it cannot receive. Bypass all permission checks for the inner claude
|
|
2177
|
+
# invocation. Worktree isolation contains the blast radius.
|
|
2178
|
+
local cmd_verbose="${cmd/claude -p/claude -p --verbose --dangerously-skip-permissions --output-format stream-json}"
|
|
2172
2179
|
# US-AUTO-037: strip leading `cd "<path>" && ` (callers like
|
|
2173
2180
|
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
2174
2181
|
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
@@ -2220,6 +2227,35 @@ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
|
2220
2227
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2221
2228
|
_USE_WORKTREE=0
|
|
2222
2229
|
cd "${project_path}" 2>/dev/null || true
|
|
2230
|
+
# FIX-040: orphan worktree recovery — scan for worktrees left by previous failed
|
|
2231
|
+
# cycles (publish failed or inner script was SIGKILL'd). Attempt to publish each
|
|
2232
|
+
# before starting the new cycle. Glob is chronological via timestamp in name.
|
|
2233
|
+
for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
2234
|
+
[ -d "\$_orphan_wt" ] || continue
|
|
2235
|
+
# Confirm it's a real worktree directory (not glob literal when no matches)
|
|
2236
|
+
[ -d "\${_orphan_wt}/.git" ] || [ -f "\${_orphan_wt}/.git" ] || continue
|
|
2237
|
+
_orphan_branch=\$(cd "\$_orphan_wt" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
2238
|
+
[ -z "\$_orphan_branch" ] && continue
|
|
2239
|
+
_orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
2240
|
+
if [ "\$_orphan_commits" -gt 0 ]; then
|
|
2241
|
+
echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
|
|
2242
|
+
_orphan_ok=0
|
|
2243
|
+
if ( cd "\$_orphan_wt" && _loop_is_doc_only_change ); then
|
|
2244
|
+
( cd "\$_orphan_wt" && _loop_publish_doc_pr "\$_orphan_branch" "doc: recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
2245
|
+
else
|
|
2246
|
+
( cd "\$_orphan_wt" && _loop_publish_pr "\$_orphan_branch" "recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
2247
|
+
fi
|
|
2248
|
+
if [ "\$_orphan_ok" -eq 1 ]; then
|
|
2249
|
+
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
2250
|
+
echo "[loop] FIX-040: orphan recovered and cleaned: \$_orphan_branch"
|
|
2251
|
+
else
|
|
2252
|
+
echo "[loop] FIX-040: orphan recovery publish failed for \$_orphan_branch — leaving preserved"
|
|
2253
|
+
fi
|
|
2254
|
+
else
|
|
2255
|
+
echo "[loop] FIX-040: orphan worktree \$_orphan_wt has no commits; cleaning up"
|
|
2256
|
+
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
2257
|
+
fi
|
|
2258
|
+
done
|
|
2223
2259
|
# US-AUTO-038: snapshot orphan claude/* branches before claude runs so the
|
|
2224
2260
|
# post-claude cleanup can diff and delete only this session's additions.
|
|
2225
2261
|
CLAUDE_BRANCH_SNAPSHOT="\$(_claude_remote_snapshot "${project_path}")"
|
|
@@ -2229,8 +2265,12 @@ if _worktree_fetch_origin main \\
|
|
|
2229
2265
|
_worktree_submodule_init "\$WT" 2>/dev/null || true
|
|
2230
2266
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
2231
2267
|
else
|
|
2232
|
-
|
|
2233
|
-
|
|
2268
|
+
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
2269
|
+
# --dangerously-skip-permissions is only safe paired with worktree isolation;
|
|
2270
|
+
# falling back to the main tree without isolation is unacceptable.
|
|
2271
|
+
_worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
|
|
2272
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
|
|
2273
|
+
exit 0
|
|
2234
2274
|
fi
|
|
2235
2275
|
|
|
2236
2276
|
FMT="${fmt_script}"
|
|
@@ -2282,12 +2322,34 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2282
2322
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2283
2323
|
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2284
2324
|
else
|
|
2285
|
-
|
|
2286
|
-
|
|
2325
|
+
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
2326
|
+
# as final safety net so code is never local-only before worktree cleanup.
|
|
2327
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
2328
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
2329
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2330
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2331
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2332
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2333
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2334
|
+
else
|
|
2335
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
|
|
2336
|
+
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
2337
|
+
fi
|
|
2287
2338
|
fi
|
|
2288
2339
|
else
|
|
2289
|
-
|
|
2290
|
-
|
|
2340
|
+
# FIX-039: PR publish failed — push orphan branch+tag to origin as safety net.
|
|
2341
|
+
# (_loop_publish_pr may have already pushed the branch; git push is idempotent.)
|
|
2342
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
2343
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
2344
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2345
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2346
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2347
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2348
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2349
|
+
else
|
|
2350
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2351
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2352
|
+
fi
|
|
2291
2353
|
fi
|
|
2292
2354
|
fi
|
|
2293
2355
|
else
|
|
@@ -2379,7 +2441,7 @@ fi
|
|
|
2379
2441
|
echo "\$\$" > "\$LOCK"
|
|
2380
2442
|
trap 'rm -f "\$LOCK"' EXIT
|
|
2381
2443
|
if command -v tmux >/dev/null 2>&1; then
|
|
2382
|
-
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop
|
|
2444
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-${slug}\$" | while read _s; do
|
|
2383
2445
|
tmux kill-session -t "\$_s" 2>/dev/null || true
|
|
2384
2446
|
done
|
|
2385
2447
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
@@ -2520,17 +2582,19 @@ _install_launchd_plists() {
|
|
|
2520
2582
|
_agent_skill_cmd() {
|
|
2521
2583
|
local skill_path="$1"
|
|
2522
2584
|
local agent; agent=$(_project_agent)
|
|
2523
|
-
# Strip YAML frontmatter inline for cron commands
|
|
2524
2585
|
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
|
-
|
|
2586
|
+
_agent_argv "$agent" plain "__PROMPT__" || {
|
|
2587
|
+
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
2588
|
+
return 1
|
|
2589
|
+
}
|
|
2590
|
+
# In cron context, use absolute claude path so a fresh shell can find it.
|
|
2591
|
+
[[ "$agent" == "claude" ]] && _AGENT_ARGV[0]="$(command -v claude 2>/dev/null || echo claude)"
|
|
2592
|
+
# Drop the prompt sentinel (always last), re-emit head args + quoted $(strip).
|
|
2593
|
+
local out="${_AGENT_ARGV[0]}" i prompt_idx=$((${#_AGENT_ARGV[@]} - 1))
|
|
2594
|
+
for ((i = 1; i < prompt_idx; i++)); do
|
|
2595
|
+
out+=" ${_AGENT_ARGV[i]}"
|
|
2596
|
+
done
|
|
2597
|
+
echo "${out} \"\$(${strip})\""
|
|
2534
2598
|
}
|
|
2535
2599
|
|
|
2536
2600
|
cmd_loop() {
|
|
@@ -2651,15 +2715,46 @@ _loop_off() {
|
|
|
2651
2715
|
ok "Loop disabled 已停用"
|
|
2652
2716
|
}
|
|
2653
2717
|
|
|
2718
|
+
_loop_is_active() {
|
|
2719
|
+
# Three-level liveness probe used by FIX-037 heal and `roll loop now`.
|
|
2720
|
+
# Returns 0 if any signal says the cycle is alive, 1 if all signals are dead.
|
|
2721
|
+
# Heartbeat is primary (FIX-038); LOCK PID and tmux session are fallbacks.
|
|
2722
|
+
# ROLL_HEARTBEAT_TIMEOUT (default 1800s) matches the outer heredoc's threshold.
|
|
2723
|
+
local slug="${1:?slug required}"
|
|
2724
|
+
local timeout="${ROLL_HEARTBEAT_TIMEOUT:-1800}"
|
|
2725
|
+
local hb_file="${_SHARED_ROOT}/loop/.heartbeat-${slug}"
|
|
2726
|
+
if [[ -f "$hb_file" ]]; then
|
|
2727
|
+
local ts; ts=$(cat "$hb_file" 2>/dev/null || echo "")
|
|
2728
|
+
if [[ "$ts" =~ ^[0-9]+$ ]]; then
|
|
2729
|
+
local age=$(( $(date -u +%s) - ts ))
|
|
2730
|
+
[[ $age -lt $timeout ]] && return 0
|
|
2731
|
+
fi
|
|
2732
|
+
fi
|
|
2733
|
+
local lock="${_SHARED_ROOT}/loop/.LOCK-${slug}"
|
|
2734
|
+
if [[ -f "$lock" ]]; then
|
|
2735
|
+
local pid; pid=$(head -1 "$lock" 2>/dev/null || echo "")
|
|
2736
|
+
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && return 0
|
|
2737
|
+
fi
|
|
2738
|
+
command -v tmux >/dev/null 2>&1 && tmux has-session -t "roll-loop-${slug}" 2>/dev/null && return 0
|
|
2739
|
+
return 1
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2654
2742
|
_loop_now() {
|
|
2743
|
+
local project_path; project_path=$(pwd -P)
|
|
2744
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2745
|
+
# Manual `roll loop now` must not bypass FIX-037 heal: if state says running
|
|
2746
|
+
# but no live signal exists, this trigger is the canonical recovery point.
|
|
2655
2747
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
2656
|
-
|
|
2748
|
+
if _loop_is_active "$slug"; then
|
|
2749
|
+
warn "Loop already running loop 正在运行中"; return 0
|
|
2750
|
+
fi
|
|
2751
|
+
info "Stale running state detected — healing before new cycle 检测到孤儿状态,正在修复..."
|
|
2752
|
+
printf "status: idle\n" > "$_LOOP_STATE"
|
|
2753
|
+
rm -f "${_SHARED_ROOT}/loop/.LOCK-${slug}" 2>/dev/null || true
|
|
2657
2754
|
fi
|
|
2658
2755
|
# Invoke the SAME runner script that launchd would invoke — same tmux,
|
|
2659
2756
|
# same --verbose, same LOCK, same auto-attach popup. ROLL_LOOP_FORCE
|
|
2660
2757
|
# 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
2758
|
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
2664
2759
|
if [[ ! -f "$runner" ]]; then
|
|
2665
2760
|
err "Runner script not found: ${runner}"
|
|
@@ -2832,6 +2927,7 @@ _loop_reset() {
|
|
|
2832
2927
|
else
|
|
2833
2928
|
info "No loop state to clear 无 loop 状态可清除"
|
|
2834
2929
|
fi
|
|
2930
|
+
rm -rf "$(_loop_heal_dir)"
|
|
2835
2931
|
}
|
|
2836
2932
|
|
|
2837
2933
|
# Suppress the auto-attach popup. When the marker file exists, runner scripts
|
|
@@ -3223,6 +3319,42 @@ EOF
|
|
|
3223
3319
|
return 1
|
|
3224
3320
|
}
|
|
3225
3321
|
|
|
3322
|
+
_loop_heal_dir() {
|
|
3323
|
+
printf '%s\n' "${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/heal"
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
# Bounded CI self-heal gate. Called by the loop SKILL when the
|
|
3327
|
+
# post-build CI check goes red. Counter is per-story, persisted under
|
|
3328
|
+
# $ROLL_LOOP_DIR/heal/<story-id>.count, so retries survive cycle boundaries.
|
|
3329
|
+
#
|
|
3330
|
+
# Exit 0: another heal attempt is allowed (counter incremented). Caller should
|
|
3331
|
+
# invoke roll-fix with the failure summary, then re-run the CI gate.
|
|
3332
|
+
# Exit 1: heal disabled (ROLL_LOOP_NO_HEAL=1) or exhausted (>= ROLL_LOOP_HEAL_MAX).
|
|
3333
|
+
# Caller should write ALERT and stop.
|
|
3334
|
+
_loop_self_heal_ci() {
|
|
3335
|
+
local story_id="$1"
|
|
3336
|
+
[[ -z "$story_id" ]] && return 1
|
|
3337
|
+
[[ "${ROLL_LOOP_NO_HEAL:-0}" == "1" ]] && return 1
|
|
3338
|
+
local max="${ROLL_LOOP_HEAL_MAX:-2}"
|
|
3339
|
+
local dir; dir=$(_loop_heal_dir)
|
|
3340
|
+
local counter="${dir}/${story_id}.count"
|
|
3341
|
+
mkdir -p "$dir"
|
|
3342
|
+
local current; current=$(<"$counter") 2>/dev/null || current=0
|
|
3343
|
+
[[ "$current" =~ ^[0-9]+$ ]] || current=0
|
|
3344
|
+
[[ "$current" -ge "$max" ]] && return 1
|
|
3345
|
+
echo $((current + 1)) > "$counter"
|
|
3346
|
+
return 0
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
# Reset per-story heal counter. Called when CI eventually turns green or by
|
|
3350
|
+
# `roll loop reset`. Idempotent.
|
|
3351
|
+
_loop_clear_heal_state() {
|
|
3352
|
+
local story_id="$1"
|
|
3353
|
+
[[ -z "$story_id" ]] && return 0
|
|
3354
|
+
rm -f "$(_loop_heal_dir)/${story_id}.count" 2>/dev/null
|
|
3355
|
+
return 0
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3226
3358
|
# Verify TCR rhythm after a story completes. Returns 0 if ok, 1 if no TCR commits.
|
|
3227
3359
|
# On failure: reverts story in BACKLOG.md to 📋 Todo and writes ALERT.
|
|
3228
3360
|
_loop_enforce_tcr() {
|
|
@@ -4245,16 +4377,6 @@ p.write_text("".join(out))
|
|
|
4245
4377
|
PYEOF
|
|
4246
4378
|
}
|
|
4247
4379
|
|
|
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
4380
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4259
4381
|
# BACKLOG — show pending tasks / manage status
|
|
4260
4382
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -4951,7 +5073,6 @@ usage() {
|
|
|
4951
5073
|
echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
|
|
4952
5074
|
echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
|
|
4953
5075
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
4954
|
-
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
4955
5076
|
echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
|
|
4956
5077
|
echo " review-pr <number> [PR Review] AI-powered code review for a PR AI 代码评审"
|
|
4957
5078
|
echo ""
|
|
@@ -4983,7 +5104,6 @@ main() {
|
|
|
4983
5104
|
backlog) cmd_backlog "$@" ;;
|
|
4984
5105
|
alert) cmd_alert "$@" ;;
|
|
4985
5106
|
agent) cmd_agent "$@" ;;
|
|
4986
|
-
release) cmd_release "$@" ;;
|
|
4987
5107
|
ci) cmd_ci "$@" ;;
|
|
4988
5108
|
review-pr) cmd_review_pr "$@" ;;
|
|
4989
5109
|
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}`)
|