@seanyao/roll 2026.528.1 → 2026.528.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 +12 -0
- package/bin/roll +210 -12
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +20 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.528.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **loop 换机器跑不会再拿过期 backlog** — 每轮自动拉最新项目元数据 `[loop]`
|
|
8
|
+
- **CI 红了 loop 不再干等** — 先试着自己修,修不好再找人 `[loop]`
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`roll loop log` 现在真的能看了** — 每轮 cycle 的留档修好了 `[loop]`
|
|
13
|
+
- **loop 跑完的终端窗口不再瞬间清空** — 关闭前能看到本轮摘要 `[loop]`
|
|
14
|
+
|
|
3
15
|
## v2026.528.1
|
|
4
16
|
|
|
5
17
|
### Added
|
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.528.
|
|
7
|
+
VERSION="2026.528.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"
|
|
@@ -747,6 +747,22 @@ _setup_snapshot() {
|
|
|
747
747
|
# _ROLL_SETUP_STATE. Caller passes the watch dir(s) plus the command + args.
|
|
748
748
|
# stdout/stderr of the inner command are suppressed (same as the previous
|
|
749
749
|
# pattern in cmd_setup) to keep the v2 UI render the only user-visible output.
|
|
750
|
+
# US-INFRA-008: ensure core.hooksPath is set to 'hooks' so TCR pre-commit gate
|
|
751
|
+
# is never silently bypassed in new clones, worktrees, or automated environments.
|
|
752
|
+
# Idempotent: already set to a non-default value → leave it (user knows better).
|
|
753
|
+
# Not a git repo → silently skip.
|
|
754
|
+
_ensure_hooks_path() {
|
|
755
|
+
local repo_path="${1:-$PWD}"
|
|
756
|
+
# Must be a git repo
|
|
757
|
+
git -C "$repo_path" rev-parse --git-dir >/dev/null 2>&1 || return 0
|
|
758
|
+
local current; current=$(git -C "$repo_path" config core.hooksPath 2>/dev/null || echo "")
|
|
759
|
+
# Only set when unset or pointing at the git default (.git/hooks)
|
|
760
|
+
if [[ -z "$current" || "$current" == ".git/hooks" ]]; then
|
|
761
|
+
git -C "$repo_path" config core.hooksPath hooks 2>/dev/null || true
|
|
762
|
+
fi
|
|
763
|
+
return 0
|
|
764
|
+
}
|
|
765
|
+
|
|
750
766
|
_run_setup_step() {
|
|
751
767
|
local watch="$1"; shift
|
|
752
768
|
local before after
|
|
@@ -803,6 +819,10 @@ cmd_setup() {
|
|
|
803
819
|
_run_setup_step "$ROLL_HOME/.peer-state" _peer_ensure_state_dir
|
|
804
820
|
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Initialize peer-review state directory"
|
|
805
821
|
|
|
822
|
+
# US-INFRA-008: configure git hooks path so TCR pre-commit gate works in this repo
|
|
823
|
+
_run_setup_step "$PWD" _ensure_hooks_path
|
|
824
|
+
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Configure git hooks path"
|
|
825
|
+
|
|
806
826
|
if command -v tmux >/dev/null 2>&1; then
|
|
807
827
|
_record skip "Ensure tmux is installed (already present)"
|
|
808
828
|
else
|
|
@@ -2719,7 +2739,7 @@ _peer_dispatch_in_tmux() {
|
|
|
2719
2739
|
{
|
|
2720
2740
|
printf '#!/bin/bash -l\n'
|
|
2721
2741
|
# FIX-050: portable PATH assembly (was hardcoded /opt/homebrew/bin)
|
|
2722
|
-
printf 'for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "$HOME/.local/bin"; do\n'
|
|
2742
|
+
printf 'for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "$HOME/.local/bin" "$HOME/.kimi-code/bin"; do\n'
|
|
2723
2743
|
printf ' case ":$PATH:" in *":$_d:"*) ;; *) [ -d "$_d" ] && PATH="$_d:$PATH" ;; esac\n'
|
|
2724
2744
|
printf 'done; export PATH\n'
|
|
2725
2745
|
printf '%s > %q 2> %q || true\n' "$cmd_str" "$out_file" "$err_file"
|
|
@@ -5563,6 +5583,8 @@ _detect_path_prepend() {
|
|
|
5563
5583
|
[[ -d /usr/local/bin ]] && dirs+=("/usr/local/bin")
|
|
5564
5584
|
[[ -d /opt/local/bin ]] && dirs+=("/opt/local/bin")
|
|
5565
5585
|
[[ -d "$HOME/.local/bin" ]] && dirs+=("$HOME/.local/bin")
|
|
5586
|
+
# FIX-129: kimi-code installs to ~/.kimi-code/bin (not brew/local), launchd misses it
|
|
5587
|
+
[[ -d "$HOME/.kimi-code/bin" ]] && dirs+=("$HOME/.kimi-code/bin")
|
|
5566
5588
|
dirs+=("/usr/bin" "/bin" "/usr/sbin" "/sbin")
|
|
5567
5589
|
for d in "${dirs[@]}"; do
|
|
5568
5590
|
case ":$seen:" in *":$d:"*) continue ;; esac
|
|
@@ -5728,7 +5750,7 @@ set -o pipefail
|
|
|
5728
5750
|
# FIX-050: portable PATH assembly — launchd/cron deliver a bare PATH that
|
|
5729
5751
|
# misses brew-installed tools (tmux, claude, node, …). Iterate candidate
|
|
5730
5752
|
# dirs; only prepend when present and not already in PATH. Idempotent.
|
|
5731
|
-
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin"; do
|
|
5753
|
+
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin" "\$HOME/.kimi-code/bin"; do
|
|
5732
5754
|
case ":\$PATH:" in *":\$_d:"*) ;; *) [ -d "\$_d" ] && PATH="\$_d:\$PATH" ;; esac
|
|
5733
5755
|
done
|
|
5734
5756
|
export PATH
|
|
@@ -6061,6 +6083,10 @@ _phase_begin startup
|
|
|
6061
6083
|
_phase_end startup ok
|
|
6062
6084
|
_phase_begin preflight
|
|
6063
6085
|
cd "${project_path}" 2>/dev/null || true
|
|
6086
|
+
# US-INFRA-008: ensure git hooks are wired so TCR pre-commit gate can't be bypassed
|
|
6087
|
+
_ensure_hooks_path "${project_path}" 2>/dev/null || true
|
|
6088
|
+
# US-LOOP-056: sync .roll/ meta from roll-meta remote before backlog scan
|
|
6089
|
+
_loop_sync_meta "${project_path}" || true
|
|
6064
6090
|
# FIX-104: GC stale merged temp branches at cycle entry — before worktree setup
|
|
6065
6091
|
# and before any early-exit gate (pre-run abort, CI red precheck). The post-claude
|
|
6066
6092
|
# call site doesn't cover those paths, so merged branches accumulated on origin.
|
|
@@ -6372,7 +6398,7 @@ INNER
|
|
|
6372
6398
|
# FIX-050: portable PATH assembly before any brew-tool lookup (tmux, caffeinate
|
|
6373
6399
|
# on some systems, claude). Mirrors the inner script's bootstrap so even when
|
|
6374
6400
|
# launchd's plist EnvironmentVariables is stale, the runner self-repairs.
|
|
6375
|
-
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin"; do
|
|
6401
|
+
for _d in /opt/homebrew/bin /usr/local/bin /opt/local/bin "\$HOME/.local/bin" "\$HOME/.kimi-code/bin"; do
|
|
6376
6402
|
case ":\$PATH:" in *":\$_d:"*) ;; *) [ -d "\$_d" ] && PATH="\$_d:\$PATH" ;; esac
|
|
6377
6403
|
done
|
|
6378
6404
|
export PATH
|
|
@@ -6459,10 +6485,24 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
6459
6485
|
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^roll-loop-${slug}\$" | while read _s; do
|
|
6460
6486
|
tmux kill-session -t "\$_s" 2>/dev/null || true
|
|
6461
6487
|
done
|
|
6462
|
-
|
|
6463
|
-
|
|
6488
|
+
# FIX-132: syntax-check the inner script before spawning the tmux session.
|
|
6489
|
+
# A heredoc quoting regression or mid-cycle regeneration can silently produce
|
|
6490
|
+
# a syntactically broken script; catching it here prevents the session from
|
|
6491
|
+
# starting in a corrupted state and logging a misleading "exited 0, retrying".
|
|
6492
|
+
if ! bash -n "\$INNER_SCRIPT" 2>>"\$LOG"; then
|
|
6493
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] ABORT: inner script failed syntax check — cycle skipped (see log: \$LOG)" >> "\$LOG"
|
|
6494
|
+
exit 1
|
|
6495
|
+
fi
|
|
6496
|
+
# FIX-130: export ROLL_CYCLE_LOG_RAW BEFORE spawning the tmux session so
|
|
6497
|
+
# the inner script inherits it (env vars are inherited at spawn time, not
|
|
6498
|
+
# retroactively — exporting after new-session means inner never sees it and
|
|
6499
|
+
# _inner_cleanup skips log archiving, leaving only orphan .pipe-*.raw files).
|
|
6464
6500
|
mkdir -p "${project_path}/.roll/cycle-logs"
|
|
6501
|
+
# Clean orphan .pipe-*.raw files from previous crashed cycles
|
|
6502
|
+
find "${project_path}/.roll/cycle-logs" -name '.pipe-*.raw' -delete 2>/dev/null || true
|
|
6503
|
+
CYCLE_LOG_RAW="${project_path}/.roll/cycle-logs/.pipe-\$\$.raw"
|
|
6465
6504
|
export ROLL_CYCLE_LOG_RAW="\$CYCLE_LOG_RAW"
|
|
6505
|
+
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
6466
6506
|
tmux pipe-pane -t "\$SESSION" "tee -a \"\$LOG\" >> \"\$ROLL_CYCLE_LOG_RAW\""
|
|
6467
6507
|
# Auto-attach popup: when not muted, spawn a Terminal.app window attached
|
|
6468
6508
|
# to the tmux session so the user can watch the loop work in real time.
|
|
@@ -6481,7 +6521,9 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
6481
6521
|
# window closes the instant the tmux session ends (cycle_end kills
|
|
6482
6522
|
# the session) and the entire scrollback disappears with it; the
|
|
6483
6523
|
# cron-<slug>.log file still has the full transcript as a fallback.
|
|
6484
|
-
|
|
6524
|
+
# FIX-131: after tmux session ends, open the cron log with less so the
|
|
6525
|
+
# user can scroll through the full cycle output instead of seeing nothing.
|
|
6526
|
+
printf '#!/bin/bash\\ntmux attach -t %s 2>/dev/null\\nLOGFILE=~/.shared/roll/loop/cron-%s.log\\necho\\nif [ -f "\$LOGFILE" ]; then\\n echo "================================================================"\\n echo " Cycle ended — showing log (arrows to scroll, q to close)"\\n echo "================================================================"\\n less -R +G "\$LOGFILE"\\nelse\\n echo "================================================================"\\n echo " Cycle ended. Log not found: \$LOGFILE"\\n echo " press enter to close."\\n echo "================================================================"\\n read _\\nfi\\n' \\
|
|
6485
6527
|
"\$SESSION" "${slug}" > "\$_attach_cmd" 2>/dev/null || true
|
|
6486
6528
|
chmod +x "\$_attach_cmd" 2>/dev/null || true
|
|
6487
6529
|
open -g -a Terminal "\$_attach_cmd" >/dev/null 2>&1 || true
|
|
@@ -6710,10 +6752,11 @@ cmd_loop() {
|
|
|
6710
6752
|
resume) _loop_resume ;;
|
|
6711
6753
|
reset) _loop_reset ;;
|
|
6712
6754
|
gc) shift; _loop_gc "$@" ;;
|
|
6713
|
-
notify)
|
|
6714
|
-
enforce-tcr)
|
|
6715
|
-
precheck-ci)
|
|
6716
|
-
|
|
6755
|
+
notify) _notify "${1:-roll}" "${2:-}" ;;
|
|
6756
|
+
enforce-tcr) _loop_enforce_tcr "${1:-}" "${2:-}" ;;
|
|
6757
|
+
precheck-ci) _loop_precheck_ci ;;
|
|
6758
|
+
hotfix-head-context) _loop_hotfix_head_context "${1:-}" ;;
|
|
6759
|
+
branches) _loop_branches "$(pwd -P)" ;;
|
|
6717
6760
|
*) cat <<'HELP'
|
|
6718
6761
|
Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mute|unmute|pause|resume|reset|gc|branches>
|
|
6719
6762
|
|
|
@@ -7741,6 +7784,66 @@ _ci_wait() {
|
|
|
7741
7784
|
}
|
|
7742
7785
|
|
|
7743
7786
|
# Pre-run CI health check — call before picking up new stories.
|
|
7787
|
+
# US-LOOP-056: sync .roll/ (roll-meta private submodule) before each cycle so
|
|
7788
|
+
# the cycle always runs against the latest backlog. Fail-soft: any error emits
|
|
7789
|
+
# a meta_sync event and returns 0 so the cycle continues with stale/existing meta.
|
|
7790
|
+
#
|
|
7791
|
+
# Statuses emitted via _loop_event meta_sync:
|
|
7792
|
+
# ok – fetch + reset --hard succeeded
|
|
7793
|
+
# stale – fetch failed; existing .roll/ used as fallback
|
|
7794
|
+
# skipped – no git remote configured (not a roll-meta managed project)
|
|
7795
|
+
#
|
|
7796
|
+
# Env override: ROLL_LOOP_META_SYNC_TIMEOUT (default 15) controls fetch timeout.
|
|
7797
|
+
_loop_sync_meta() {
|
|
7798
|
+
local project_path="$1"
|
|
7799
|
+
local roll_meta="${project_path}/.roll"
|
|
7800
|
+
local timeout_sec="${ROLL_LOOP_META_SYNC_TIMEOUT:-15}"
|
|
7801
|
+
local cid="${CYCLE_ID:-unknown}"
|
|
7802
|
+
local slug="${_LOOP_PROJ_SLUG:-$(_project_slug 2>/dev/null || echo unknown)}"
|
|
7803
|
+
local shared_dir="${_SHARED_ROOT:-$HOME/.shared/roll}/loop"
|
|
7804
|
+
local fail_counter="${shared_dir}/meta-sync-fail-${slug}"
|
|
7805
|
+
|
|
7806
|
+
# Detect remote via the canonical probe point. If .roll/ has no .git or no
|
|
7807
|
+
# remote configured, treat as "not managed" and skip silently.
|
|
7808
|
+
local remote_url
|
|
7809
|
+
remote_url=$(git -C "$roll_meta" remote get-url origin 2>/dev/null || echo "")
|
|
7810
|
+
if [ -z "$remote_url" ]; then
|
|
7811
|
+
return 0
|
|
7812
|
+
fi
|
|
7813
|
+
|
|
7814
|
+
# Attempt fetch with timeout
|
|
7815
|
+
local _fetch_ok=0
|
|
7816
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
7817
|
+
timeout "$timeout_sec" git -C "$roll_meta" fetch --quiet 2>/dev/null && _fetch_ok=1
|
|
7818
|
+
else
|
|
7819
|
+
git -C "$roll_meta" fetch --quiet 2>/dev/null && _fetch_ok=1
|
|
7820
|
+
fi
|
|
7821
|
+
|
|
7822
|
+
if [ "$_fetch_ok" -eq 1 ]; then
|
|
7823
|
+
if git -C "$roll_meta" reset --hard origin/main --quiet 2>/dev/null; then
|
|
7824
|
+
_loop_event meta_sync "$cid" "ok" "" 2>/dev/null || true
|
|
7825
|
+
# US-LOOP-057: reset consecutive failure counter on success
|
|
7826
|
+
rm -f "$fail_counter" 2>/dev/null || true
|
|
7827
|
+
return 0
|
|
7828
|
+
fi
|
|
7829
|
+
fi
|
|
7830
|
+
|
|
7831
|
+
# Fetch or reset failed — stale .roll/ used; cycle continues
|
|
7832
|
+
_loop_event meta_sync "$cid" "stale" "fetch/reset failed" 2>/dev/null || true
|
|
7833
|
+
|
|
7834
|
+
# US-LOOP-057: increment failure counter; write ALERT after 3 consecutive failures
|
|
7835
|
+
mkdir -p "$shared_dir" 2>/dev/null || true
|
|
7836
|
+
local count=0
|
|
7837
|
+
[ -f "$fail_counter" ] && count=$(cat "$fail_counter" 2>/dev/null || echo 0)
|
|
7838
|
+
count=$(( count + 1 ))
|
|
7839
|
+
printf '%s\n' "$count" > "$fail_counter"
|
|
7840
|
+
if [ "$count" -ge 3 ]; then
|
|
7841
|
+
printf '[%s] roll-meta sync consecutive failures: %d times. Check SSH key / network.\n Last error: fetch/reset failed for %s\n' \
|
|
7842
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$count" "$remote_url" >> "${shared_dir}/ALERT-${slug}.md" 2>/dev/null || true
|
|
7843
|
+
fi
|
|
7844
|
+
return 0
|
|
7845
|
+
}
|
|
7846
|
+
|
|
7744
7847
|
# Refuses to build on a red base (HEAD CI failed). Lenient on unknown states
|
|
7745
7848
|
# (gh missing, repo unparseable, no runs yet) — the post-build _loop_enforce_ci
|
|
7746
7849
|
# is the strict gate.
|
|
@@ -7773,6 +7876,38 @@ _loop_precheck_ci() {
|
|
|
7773
7876
|
run_states=$(echo "$runs" \
|
|
7774
7877
|
| jq -r '[.[] | "\(.status // "?")/\(.conclusion // "null")"] | unique | join(", ")' \
|
|
7775
7878
|
2>/dev/null || echo "?")
|
|
7879
|
+
|
|
7880
|
+
# US-LOOP-046/048: check whether hot-fix path is allowed before aborting.
|
|
7881
|
+
# ROLL_LOOP_NO_HEAL=1 or ROLL_LOOP_HEAL_MAX=0 → fall through to original abort.
|
|
7882
|
+
local _heal_max="${ROLL_LOOP_HEAL_MAX:-2}"
|
|
7883
|
+
if [[ "${ROLL_LOOP_NO_HEAL:-}" != "1" ]] && [[ "$_heal_max" -gt 0 ]]; then
|
|
7884
|
+
local _state_file="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/state-${_LOOP_PROJ_SLUG:-$(basename "$PWD")}.yaml"
|
|
7885
|
+
local _heal_key="heal_count_head_${commit:0:8}"
|
|
7886
|
+
local _count=0
|
|
7887
|
+
[[ -f "$_state_file" ]] && _count=$(grep "^${_heal_key}:" "$_state_file" 2>/dev/null | awk '{print $2}' || echo 0)
|
|
7888
|
+
_count=$(( ${_count:-0} + 0 )) # coerce to int
|
|
7889
|
+
if [[ "$_count" -lt "$_heal_max" ]]; then
|
|
7890
|
+
# Increment counter and signal hot-fix path to the agent
|
|
7891
|
+
_count=$(( _count + 1 ))
|
|
7892
|
+
mkdir -p "$(dirname "$_state_file")" 2>/dev/null || true
|
|
7893
|
+
if [[ -f "$_state_file" ]]; then
|
|
7894
|
+
# Update existing key or append
|
|
7895
|
+
if grep -q "^${_heal_key}:" "$_state_file" 2>/dev/null; then
|
|
7896
|
+
local _tmp; _tmp=$(mktemp)
|
|
7897
|
+
grep -v "^${_heal_key}:" "$_state_file" > "$_tmp" 2>/dev/null || true
|
|
7898
|
+
printf '%s: %d\n' "$_heal_key" "$_count" >> "$_tmp"
|
|
7899
|
+
mv "$_tmp" "$_state_file"
|
|
7900
|
+
else
|
|
7901
|
+
printf '%s: %d\n' "$_heal_key" "$_count" >> "$_state_file"
|
|
7902
|
+
fi
|
|
7903
|
+
else
|
|
7904
|
+
printf '%s: %d\n' "$_heal_key" "$_count" > "$_state_file"
|
|
7905
|
+
fi
|
|
7906
|
+
# Exit 2 signals the agent: CI is red, hot-fix path is available
|
|
7907
|
+
return 2
|
|
7908
|
+
fi
|
|
7909
|
+
fi
|
|
7910
|
+
|
|
7776
7911
|
err "$(msg loop.pre_run_ci_check_head_ci ${short})"
|
|
7777
7912
|
mkdir -p "$(dirname "$_LOOP_ALERT")"
|
|
7778
7913
|
cat > "$_LOOP_ALERT" << EOF
|
|
@@ -7795,6 +7930,62 @@ EOF
|
|
|
7795
7930
|
return 0
|
|
7796
7931
|
}
|
|
7797
7932
|
|
|
7933
|
+
# US-LOOP-047: hot-fix context factory for HEAD CI failures.
|
|
7934
|
+
# Captures failing run logs + recent commit diff, writes to /tmp/roll-heal-head-<sha>.log
|
|
7935
|
+
# Returns 0 and prints the log path on success; 1 if context could not be gathered.
|
|
7936
|
+
_loop_hotfix_head_context() {
|
|
7937
|
+
local commit="${1:-$(git rev-parse HEAD 2>/dev/null)}"
|
|
7938
|
+
[[ -z "$commit" ]] && return 1
|
|
7939
|
+
local short="${commit:0:8}"
|
|
7940
|
+
local outfile="/tmp/roll-heal-head-${short}.log"
|
|
7941
|
+
local slug; _gh_resolve slug || slug="unknown"
|
|
7942
|
+
|
|
7943
|
+
{
|
|
7944
|
+
printf '=== CI Hot-fix Context: HEAD %s ===\n\n' "$short"
|
|
7945
|
+
printf '--- Recent commits ---\n'
|
|
7946
|
+
git log --oneline -5 2>/dev/null || true
|
|
7947
|
+
printf '\n--- Diff of last commit ---\n'
|
|
7948
|
+
git show --stat HEAD 2>/dev/null | head -40 || true
|
|
7949
|
+
printf '\n--- CI failure logs (head 200 lines) ---\n'
|
|
7950
|
+
local run_id
|
|
7951
|
+
run_id=$(gh -R "$slug" run list --commit "$commit" \
|
|
7952
|
+
--json databaseId,conclusion -L 5 2>/dev/null \
|
|
7953
|
+
| jq -r '.[] | select(.conclusion=="failure") | .databaseId' 2>/dev/null | head -1)
|
|
7954
|
+
if [[ -n "$run_id" ]]; then
|
|
7955
|
+
gh -R "$slug" run view --log-failed "$run_id" 2>/dev/null | head -200 || true
|
|
7956
|
+
else
|
|
7957
|
+
printf '(no failed run found for commit %s)\n' "$short"
|
|
7958
|
+
fi
|
|
7959
|
+
} > "$outfile" 2>/dev/null
|
|
7960
|
+
printf '%s\n' "$outfile"
|
|
7961
|
+
return 0
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
# US-LOOP-050: PR hot-fix entry point.
|
|
7965
|
+
# Checks out the PR branch, captures CI failure logs, and prepares context
|
|
7966
|
+
# for a roll-fix invocation on the PR branch.
|
|
7967
|
+
# Usage: _loop_hot_fix_pr <pr_number>
|
|
7968
|
+
_loop_hot_fix_pr() {
|
|
7969
|
+
local pr_num="$1"
|
|
7970
|
+
[[ -z "$pr_num" ]] && return 1
|
|
7971
|
+
local slug; _gh_resolve slug || return 1
|
|
7972
|
+
local outfile="/tmp/roll-heal-pr-${pr_num}.log"
|
|
7973
|
+
local run_id
|
|
7974
|
+
run_id=$(gh -R "$slug" run list --json databaseId,conclusion,headBranch -L 20 2>/dev/null \
|
|
7975
|
+
| jq -r --argjson pr "\"$pr_num\"" \
|
|
7976
|
+
'.[] | select(.conclusion=="failure") | .databaseId' 2>/dev/null | head -1)
|
|
7977
|
+
{
|
|
7978
|
+
printf '=== PR #%s CI Hot-fix Context ===\n\n' "$pr_num"
|
|
7979
|
+
if [[ -n "$run_id" ]]; then
|
|
7980
|
+
gh -R "$slug" run view --log-failed "$run_id" 2>/dev/null | head -200 || true
|
|
7981
|
+
else
|
|
7982
|
+
printf '(no failed run found for PR #%s)\n' "$pr_num"
|
|
7983
|
+
fi
|
|
7984
|
+
} > "$outfile" 2>/dev/null
|
|
7985
|
+
printf '%s\n' "$outfile"
|
|
7986
|
+
return 0
|
|
7987
|
+
}
|
|
7988
|
+
|
|
7798
7989
|
# _loop_diagnose_open_prs <slug>
|
|
7799
7990
|
# Appended to ALERT when CI is red on HEAD.
|
|
7800
7991
|
# For each open PR targeting main: lists CI failing tests + changed files,
|
|
@@ -8029,7 +8220,14 @@ _loop_pr_classify() {
|
|
|
8029
8220
|
local mergeable="${4:-}"
|
|
8030
8221
|
|
|
8031
8222
|
case "$head_ref" in
|
|
8032
|
-
loop/*)
|
|
8223
|
+
loop/*)
|
|
8224
|
+
# US-LOOP-049: loop/* PRs with CI failure get their own classification
|
|
8225
|
+
# so _loop_pr_inbox can route them to the PR hot-fix path.
|
|
8226
|
+
if [[ "$ci_state" == "failure" ]]; then
|
|
8227
|
+
echo "loop_self_ci_red"; return 0
|
|
8228
|
+
fi
|
|
8229
|
+
echo "loop_self"; return 0
|
|
8230
|
+
;;
|
|
8033
8231
|
esac
|
|
8034
8232
|
|
|
8035
8233
|
case "$human_review" in
|
package/package.json
CHANGED
|
@@ -125,16 +125,26 @@ readers. The rule mirrors the gate in Step 2.
|
|
|
125
125
|
### Step 1.5 — Pre-run CI Health Check
|
|
126
126
|
|
|
127
127
|
Call `roll loop precheck-ci` before scanning BACKLOG. This is a **defensive gate**
|
|
128
|
-
against building on a broken base
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
against building on a broken base. Check the **exit code** and route accordingly:
|
|
129
|
+
|
|
130
|
+
| Exit code | Meaning | Action |
|
|
131
|
+
|-----------|---------|--------|
|
|
132
|
+
| `0` | CI green / pending / unknown | Proceed to Step 1.6 (PR Inbox) and Step 2 (BACKLOG scan) |
|
|
133
|
+
| `1` | CI red AND heal exhausted or `ROLL_LOOP_NO_HEAL=1` | ALERT already written; exit cleanly this cycle |
|
|
134
|
+
| `2` | CI red AND heal attempt allowed (US-LOOP-046) | **Hot-fix path** — skip BACKLOG, fix CI instead (see below) |
|
|
135
|
+
|
|
136
|
+
`gh` missing or repo unparseable → `precheck-ci` returns `0`; graceful skip.
|
|
137
|
+
|
|
138
|
+
**Hot-fix path (exit code 2) — US-LOOP-046:**
|
|
139
|
+
|
|
140
|
+
Do NOT pick any BACKLOG stories this cycle. Instead:
|
|
141
|
+
|
|
142
|
+
1. Capture context: `roll loop hotfix-head-context` → prints path to context log
|
|
143
|
+
2. Invoke `Skill("roll-fix")` with brief:
|
|
144
|
+
`"CI red on HEAD. Failing run logs at <context-log-path>. Diagnose root cause, fix via TCR, commit, push. Do NOT change BACKLOG status."`
|
|
145
|
+
3. After `roll-fix` completes, re-run `roll ci --wait` to verify the fix
|
|
146
|
+
4. If CI is still red: run `roll loop precheck-ci` again; if it returns `1` (heal exhausted),
|
|
147
|
+
exit cleanly — ALERT was already written by the precheck
|
|
138
148
|
|
|
139
149
|
### Step 1.6 — PR Inbox (US-AUTO-034)
|
|
140
150
|
|