@seanyao/roll 2026.512.6 → 2026.512.8
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 -0
- package/README.md +39 -225
- package/bin/roll +312 -36
- package/package.json +2 -2
- package/skills/roll-.changelog/SKILL.md +24 -2
- package/skills/roll-.dream/SKILL.md +127 -4
- package/skills/roll-brief/SKILL.md +9 -0
- package/skills/roll-build/SKILL.md +10 -6
- package/skills/roll-doc/SKILL.md +184 -0
- package/skills/roll-loop/SKILL.md +38 -6
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.512.
|
|
7
|
+
VERSION="2026.512.8"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -117,6 +117,18 @@ _get_ai_tools() {
|
|
|
117
117
|
done
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
# Iterate all configured AI tools, calling: callback entry ai_dir ai_config ai_src [extra_args...]
|
|
121
|
+
_for_each_ai_tool() {
|
|
122
|
+
local _feach_cb="$1"; shift
|
|
123
|
+
while IFS= read -r _feach_entry; do
|
|
124
|
+
"$_feach_cb" "$_feach_entry" \
|
|
125
|
+
"$(_ai_dir "$_feach_entry")" \
|
|
126
|
+
"$(_ai_config "$_feach_entry")" \
|
|
127
|
+
"$(_ai_src "$_feach_entry")" \
|
|
128
|
+
"$@"
|
|
129
|
+
done < <(_get_ai_tools)
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
# Add any ai_* keys from the default set that are missing from the user's config.
|
|
121
133
|
# Non-destructive: never removes or modifies existing entries.
|
|
122
134
|
_ensure_config_entries() {
|
|
@@ -505,16 +517,14 @@ _sync_convention_for_tool() {
|
|
|
505
517
|
fi
|
|
506
518
|
}
|
|
507
519
|
|
|
520
|
+
_sync_one_tool() {
|
|
521
|
+
local _entry="$1" _ai_dir="$2" _cfg="$3" _src="$4" force="$5"
|
|
522
|
+
_sync_convention_for_tool "$ROLL_GLOBAL/$_src" "$_ai_dir/$_cfg" "$force"
|
|
523
|
+
}
|
|
524
|
+
|
|
508
525
|
_sync_conventions() {
|
|
509
526
|
local force="${1:-false}"
|
|
510
|
-
|
|
511
|
-
while IFS= read -r entry; do
|
|
512
|
-
local ai_dir config_file src_file
|
|
513
|
-
ai_dir="$(_ai_dir "$entry")"
|
|
514
|
-
config_file="$(_ai_config "$entry")"
|
|
515
|
-
src_file="$(_ai_src "$entry")"
|
|
516
|
-
_sync_convention_for_tool "$ROLL_GLOBAL/$src_file" "$ai_dir/$config_file" "$force"
|
|
517
|
-
done < <(_get_ai_tools)
|
|
527
|
+
_for_each_ai_tool _sync_one_tool "$force"
|
|
518
528
|
}
|
|
519
529
|
|
|
520
530
|
# ─── Internal: sync skills (pull + link) ──────────────────────────────────────
|
|
@@ -1038,22 +1048,10 @@ scan_project_type_from_files() {
|
|
|
1038
1048
|
fi
|
|
1039
1049
|
}
|
|
1040
1050
|
|
|
1041
|
-
# ─── Helper: true when cwd has no existing source code ───────────────────────
|
|
1042
|
-
is_fresh_project() {
|
|
1043
|
-
local dir="${1:-.}"
|
|
1044
|
-
! ( [[ -f "$dir/package.json" ]] || [[ -f "$dir/go.mod" ]] || \
|
|
1045
|
-
[[ -f "$dir/Cargo.toml" ]] || [[ -f "$dir/requirements.txt" ]] || \
|
|
1046
|
-
[[ -f "$dir/pyproject.toml" ]] || \
|
|
1047
|
-
[[ -d "$dir/src" ]] || [[ -d "$dir/api" ]] || [[ -d "$dir/app" ]] )
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
# ─── Helper: make a scaffold dir + .gitkeep ───────────────────────────────────
|
|
1051
|
-
_mkscaffold() { mkdir -p "$1"; touch "$1/.gitkeep"; }
|
|
1052
|
-
|
|
1053
1051
|
# ─── Helper: write starter BACKLOG.md (no-op if exists) ──────────────────────
|
|
1054
1052
|
_write_backlog() {
|
|
1055
1053
|
if [[ -f "$1" ]]; then
|
|
1056
|
-
|
|
1054
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|BACKLOG.md")
|
|
1057
1055
|
return
|
|
1058
1056
|
fi
|
|
1059
1057
|
cat > "$1" << 'EOF'
|
|
@@ -1068,18 +1066,18 @@ _write_backlog() {
|
|
|
1068
1066
|
|----|---------|--------|
|
|
1069
1067
|
EOF
|
|
1070
1068
|
ok "Created: BACKLOG.md"
|
|
1071
|
-
|
|
1069
|
+
_ROLL_MERGE_SUMMARY+=("created|BACKLOG.md")
|
|
1072
1070
|
}
|
|
1073
1071
|
|
|
1074
1072
|
_ensure_features_dir() {
|
|
1075
1073
|
if [[ -d "$1" ]]; then
|
|
1076
|
-
|
|
1074
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|docs/features/")
|
|
1077
1075
|
return
|
|
1078
1076
|
fi
|
|
1079
1077
|
|
|
1080
1078
|
mkdir -p "$1"
|
|
1081
1079
|
ok "Created: docs/features/"
|
|
1082
|
-
|
|
1080
|
+
_ROLL_MERGE_SUMMARY+=("created|docs/features/")
|
|
1083
1081
|
}
|
|
1084
1082
|
|
|
1085
1083
|
# ─── Helper: write starter .gitignore (no-op if exists) ──────────────────────
|
|
@@ -2266,7 +2264,8 @@ cmd_loop() {
|
|
|
2266
2264
|
pause) _loop_pause ;;
|
|
2267
2265
|
resume) _loop_resume ;;
|
|
2268
2266
|
reset) _loop_reset ;;
|
|
2269
|
-
|
|
2267
|
+
notify) _notify "${1:-roll}" "${2:-}" ;;
|
|
2268
|
+
*) err "Usage: roll loop <on|off|now|test|status|monitor|runs|attach|mute|unmute|pause|resume|reset|notify>"; exit 1 ;;
|
|
2270
2269
|
esac
|
|
2271
2270
|
}
|
|
2272
2271
|
|
|
@@ -2484,7 +2483,7 @@ _loop_status() {
|
|
|
2484
2483
|
else
|
|
2485
2484
|
echo -e " Auto-attach ${GREEN}live${NC} run: roll loop mute"
|
|
2486
2485
|
fi
|
|
2487
|
-
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT
|
|
2486
|
+
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT${NC} (${CYAN}roll alert${NC} to manage)"; sed 's/^/ /' "$_LOOP_ALERT"; }
|
|
2488
2487
|
[[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
|
|
2489
2488
|
echo ""
|
|
2490
2489
|
}
|
|
@@ -2596,11 +2595,12 @@ _loop_runs_dur() {
|
|
|
2596
2595
|
}
|
|
2597
2596
|
|
|
2598
2597
|
# Format a single JSONL run record for display.
|
|
2598
|
+
# Reads _LOOP_RUNS_BACKLOG global for ID→description lookup (set by _loop_runs).
|
|
2599
2599
|
_loop_runs_format_line() {
|
|
2600
|
-
local line="$1" show_project="$2"
|
|
2600
|
+
local line="$1" show_project="$2" is_darwin="$3"
|
|
2601
2601
|
command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
|
|
2602
2602
|
|
|
2603
|
-
local ts status project tcr duration alerts run_id reason built_count
|
|
2603
|
+
local ts status project tcr duration alerts run_id reason built_count skipped_count
|
|
2604
2604
|
ts=$(jq -r '.ts // ""' <<<"$line")
|
|
2605
2605
|
status=$(jq -r '.status // ""' <<<"$line")
|
|
2606
2606
|
project=$(jq -r '.project // ""' <<<"$line")
|
|
@@ -2610,10 +2610,16 @@ _loop_runs_format_line() {
|
|
|
2610
2610
|
run_id=$(jq -r '.run_id // ""' <<<"$line")
|
|
2611
2611
|
reason=$(jq -r '.reason // ""' <<<"$line")
|
|
2612
2612
|
built_count=$(jq -r '(.built // []) | length' <<<"$line")
|
|
2613
|
-
built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
|
|
2614
2613
|
skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
|
|
2615
2614
|
|
|
2616
|
-
local hhmm
|
|
2615
|
+
local hhmm epoch=""
|
|
2616
|
+
if [[ "$is_darwin" == "1" ]]; then
|
|
2617
|
+
epoch=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$ts" "+%s" 2>/dev/null) || epoch=""
|
|
2618
|
+
[[ -n "$epoch" ]] && hhmm=$(date -j -f "%s" "$epoch" "+%H:%M" 2>/dev/null) || hhmm=""
|
|
2619
|
+
else
|
|
2620
|
+
hhmm=$(date -d "$ts" "+%H:%M" 2>/dev/null) || hhmm=""
|
|
2621
|
+
fi
|
|
2622
|
+
[[ -z "$hhmm" ]] && hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
|
|
2617
2623
|
local prefix=""
|
|
2618
2624
|
if [[ "$show_project" == "true" ]]; then
|
|
2619
2625
|
prefix="[$(basename "$project")] "
|
|
@@ -2623,8 +2629,29 @@ _loop_runs_format_line() {
|
|
|
2623
2629
|
built)
|
|
2624
2630
|
local skipped_note=""
|
|
2625
2631
|
[[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
|
|
2626
|
-
|
|
2627
|
-
|
|
2632
|
+
local items_word; [[ "$built_count" -eq 1 ]] && items_word="item" || items_word="items"
|
|
2633
|
+
printf " %s %s✅ built %d %s (%d tcr%s, %s)\n" \
|
|
2634
|
+
"$hhmm" "$prefix" "$built_count" "$items_word" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
|
|
2635
|
+
local id desc
|
|
2636
|
+
while IFS= read -r id; do
|
|
2637
|
+
[[ -z "$id" ]] && continue
|
|
2638
|
+
desc=""
|
|
2639
|
+
if [[ -n "$_LOOP_RUNS_BACKLOG" ]]; then
|
|
2640
|
+
desc=$(printf "%s\n" "$_LOOP_RUNS_BACKLOG" | awk -F'|' -v id="$id" '
|
|
2641
|
+
NF >= 3 {
|
|
2642
|
+
cell = $2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", cell)
|
|
2643
|
+
if (cell == id || cell ~ "^\\[" id "\\]") {
|
|
2644
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3); print $3; exit
|
|
2645
|
+
}
|
|
2646
|
+
}')
|
|
2647
|
+
fi
|
|
2648
|
+
if [[ -n "$desc" ]]; then
|
|
2649
|
+
[[ ${#desc} -gt 72 ]] && desc="${desc:0:69}..."
|
|
2650
|
+
printf " • %-14s %s\n" "$id" "$desc"
|
|
2651
|
+
else
|
|
2652
|
+
printf " • %s\n" "$id"
|
|
2653
|
+
fi
|
|
2654
|
+
done < <(jq -r '(.built // []) | .[]' <<<"$line")
|
|
2628
2655
|
;;
|
|
2629
2656
|
idle)
|
|
2630
2657
|
printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
|
|
@@ -2677,10 +2704,27 @@ _loop_runs() {
|
|
|
2677
2704
|
local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
|
|
2678
2705
|
local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
|
|
2679
2706
|
|
|
2707
|
+
local _is_darwin=""
|
|
2708
|
+
[[ "$(uname)" == "Darwin" ]] && _is_darwin="1"
|
|
2709
|
+
|
|
2710
|
+
_LOOP_RUNS_BACKLOG=""
|
|
2711
|
+
[[ -f "$project_path/BACKLOG.md" ]] && _LOOP_RUNS_BACKLOG=$(cat "$project_path/BACKLOG.md")
|
|
2712
|
+
|
|
2680
2713
|
while IFS= read -r line; do
|
|
2681
2714
|
[[ -z "$line" ]] && continue
|
|
2682
|
-
_loop_runs_format_line "$line" "$all_flag"
|
|
2715
|
+
_loop_runs_format_line "$line" "$all_flag" "$_is_darwin"
|
|
2683
2716
|
done <<<"$recent"
|
|
2717
|
+
unset _LOOP_RUNS_BACKLOG
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
# Send a macOS system notification. No-op when muted, non-macOS, or osascript unavailable.
|
|
2721
|
+
_notify() {
|
|
2722
|
+
local title="${1:-roll}"
|
|
2723
|
+
local body="${2:-}"
|
|
2724
|
+
[ "$(uname)" = "Darwin" ] || return 0
|
|
2725
|
+
[ -f "$_LOOP_MUTE_FILE" ] && return 0
|
|
2726
|
+
command -v osascript >/dev/null 2>&1 || return 0
|
|
2727
|
+
osascript -e "display notification \"${body}\" with title \"${title}\"" >/dev/null 2>&1 || true
|
|
2684
2728
|
}
|
|
2685
2729
|
|
|
2686
2730
|
# Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
|
|
@@ -2690,6 +2734,152 @@ _loop_tcr_count() {
|
|
|
2690
2734
|
| awk '/^[a-f0-9]+ tcr:/{n++} END{print n+0}'
|
|
2691
2735
|
}
|
|
2692
2736
|
|
|
2737
|
+
# Parse origin remote URL → "owner/repo" for GitHub repos.
|
|
2738
|
+
# Returns non-zero if no origin, or origin is not github.com.
|
|
2739
|
+
# Decoupled from `gh` auto-detection so SSH config host rewrites don't break it.
|
|
2740
|
+
_gh_repo_slug() {
|
|
2741
|
+
local url
|
|
2742
|
+
url=$(git config --get remote.origin.url 2>/dev/null) || return 1
|
|
2743
|
+
case "$url" in
|
|
2744
|
+
git@github.com:*) url="${url#git@github.com:}" ;;
|
|
2745
|
+
ssh://git@github.com/*) url="${url#ssh://git@github.com/}" ;;
|
|
2746
|
+
https://github.com/*) url="${url#https://github.com/}" ;;
|
|
2747
|
+
http://github.com/*) url="${url#http://github.com/}" ;;
|
|
2748
|
+
*) return 1 ;;
|
|
2749
|
+
esac
|
|
2750
|
+
url="${url%.git}"
|
|
2751
|
+
[[ -z "$url" ]] && return 1
|
|
2752
|
+
printf "%s\n" "$url"
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
# Poll gh run list until current commit's CI completes.
|
|
2756
|
+
# Returns 0 on success (or when gh binary missing — graceful skip).
|
|
2757
|
+
# Returns 1 on CI failure, timeout, or any gh call failure.
|
|
2758
|
+
_ci_wait() {
|
|
2759
|
+
local timeout="${1:-300}"
|
|
2760
|
+
local interval=15
|
|
2761
|
+
local elapsed=0
|
|
2762
|
+
|
|
2763
|
+
command -v gh &>/dev/null || { warn "gh not installed — skipping CI gate gh 未安装,跳过 CI 检查"; return 0; }
|
|
2764
|
+
|
|
2765
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
|
|
2766
|
+
local short; short=$(git rev-parse --short HEAD 2>/dev/null)
|
|
2767
|
+
|
|
2768
|
+
# Resolve owner/repo from git remote so we don't depend on gh's auto-detection,
|
|
2769
|
+
# which breaks when ~/.ssh/config rewrites `Host github.com` → IP address.
|
|
2770
|
+
local repo_slug; repo_slug=$(_gh_repo_slug) || {
|
|
2771
|
+
err "Cannot determine GitHub repo from origin remote 无法从 origin 推导 GitHub 仓库"
|
|
2772
|
+
return 1
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
ok "Waiting for CI on ${short} (${repo_slug}) 等待 CI: ${short}"
|
|
2776
|
+
|
|
2777
|
+
while (( elapsed < timeout )); do
|
|
2778
|
+
local runs
|
|
2779
|
+
runs=$(gh -R "$repo_slug" run list --commit "$commit" --json status,conclusion 2>&1) || {
|
|
2780
|
+
err "gh run list failed for ${repo_slug}@${short}: ${runs} gh 调用失败"
|
|
2781
|
+
return 1
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
if [[ -z "$runs" || "$runs" == "[]" ]]; then
|
|
2785
|
+
(( elapsed == 0 )) && echo " No CI runs found yet, waiting... 尚无 CI 记录,等待触发..."
|
|
2786
|
+
sleep "$interval"
|
|
2787
|
+
elapsed=$(( elapsed + interval ))
|
|
2788
|
+
continue
|
|
2789
|
+
fi
|
|
2790
|
+
|
|
2791
|
+
local pending
|
|
2792
|
+
pending=$(echo "$runs" | jq -r '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
|
|
2793
|
+
|
|
2794
|
+
if [[ "$pending" -gt 0 ]]; then
|
|
2795
|
+
printf " ⏳ CI running (%ds)... CI 运行中(%ds)...\n" "$elapsed" "$elapsed"
|
|
2796
|
+
sleep "$interval"
|
|
2797
|
+
elapsed=$(( elapsed + interval ))
|
|
2798
|
+
continue
|
|
2799
|
+
fi
|
|
2800
|
+
|
|
2801
|
+
local failed
|
|
2802
|
+
failed=$(echo "$runs" | jq -r '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != null)] | length' 2>/dev/null || echo "0")
|
|
2803
|
+
|
|
2804
|
+
if [[ "$failed" -gt 0 ]]; then
|
|
2805
|
+
err "CI failed for ${short} CI 失败: ${short}"
|
|
2806
|
+
return 1
|
|
2807
|
+
fi
|
|
2808
|
+
|
|
2809
|
+
ok "CI passed CI 通过"
|
|
2810
|
+
return 0
|
|
2811
|
+
done
|
|
2812
|
+
|
|
2813
|
+
warn "CI timed out after ${timeout}s CI 等待超时(${timeout}s)"
|
|
2814
|
+
return 1
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
# Pre-run CI health check — call before picking up new stories.
|
|
2818
|
+
# Refuses to build on a red base (HEAD CI failed). Lenient on unknown states
|
|
2819
|
+
# (gh missing, repo unparseable, no runs yet) — the post-build _loop_enforce_ci
|
|
2820
|
+
# is the strict gate.
|
|
2821
|
+
# Returns 0: ok to proceed (green / pending / unknown / no gh).
|
|
2822
|
+
# Returns 1: HEAD CI is definitively red → ALERT written, do not build.
|
|
2823
|
+
_loop_precheck_ci() {
|
|
2824
|
+
command -v gh &>/dev/null || return 0
|
|
2825
|
+
|
|
2826
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || return 0
|
|
2827
|
+
local slug; slug=$(_gh_repo_slug) || return 0
|
|
2828
|
+
|
|
2829
|
+
local runs
|
|
2830
|
+
runs=$(gh -R "$slug" run list --commit "$commit" --json conclusion 2>/dev/null) || return 0
|
|
2831
|
+
[[ -z "$runs" || "$runs" == "[]" ]] && return 0
|
|
2832
|
+
|
|
2833
|
+
local failed
|
|
2834
|
+
failed=$(echo "$runs" | jq -r '[.[] | select(.conclusion != null and .conclusion != "success" and .conclusion != "skipped")] | length' 2>/dev/null || echo "0")
|
|
2835
|
+
|
|
2836
|
+
if [[ "$failed" -gt 0 ]]; then
|
|
2837
|
+
local short; short=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
|
2838
|
+
err "Pre-run CI check: HEAD CI is red — refuse to build on broken base (${short}) HEAD CI 红,拒绝在破损的基础上构建"
|
|
2839
|
+
mkdir -p "$(dirname "$_LOOP_ALERT")"
|
|
2840
|
+
cat > "$_LOOP_ALERT" << EOF
|
|
2841
|
+
# ALERT — Pre-run CI check failed (red base)
|
|
2842
|
+
|
|
2843
|
+
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
2844
|
+
**Commit**: ${short}
|
|
2845
|
+
**Reason**: HEAD CI is red — loop refused to build on a broken base HEAD CI 红,loop 拒绝在破损的基础上构建
|
|
2846
|
+
|
|
2847
|
+
**Action required**:
|
|
2848
|
+
- Investigate and fix CI: \`gh -R $(_gh_repo_slug) run list --commit ${commit}\`
|
|
2849
|
+
- After fixing and pushing green commit: \`roll loop now\`
|
|
2850
|
+
EOF
|
|
2851
|
+
_notify "roll ⚠ CI red" "loop refused to build on broken base (${short})"
|
|
2852
|
+
return 1
|
|
2853
|
+
fi
|
|
2854
|
+
return 0
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
# CI gate before marking a story Done.
|
|
2858
|
+
# On CI failure: writes ALERT, returns 1 (caller keeps story 🔨 In Progress).
|
|
2859
|
+
# When gh unavailable: returns 0 (graceful skip).
|
|
2860
|
+
_loop_enforce_ci() {
|
|
2861
|
+
local story_id="$1"
|
|
2862
|
+
|
|
2863
|
+
_ci_wait 300 && return 0
|
|
2864
|
+
|
|
2865
|
+
mkdir -p "$(dirname "$_LOOP_ALERT")"
|
|
2866
|
+
cat > "$_LOOP_ALERT" << EOF
|
|
2867
|
+
# ALERT — CI gate failed
|
|
2868
|
+
|
|
2869
|
+
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
2870
|
+
**Story**: ${story_id}
|
|
2871
|
+
**Commit**: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
|
2872
|
+
**Reason**: CI did not pass — story kept as 🔨 In Progress CI 未通过,故事保持进行中
|
|
2873
|
+
|
|
2874
|
+
**Action required** (choose one):
|
|
2875
|
+
- Fix CI and re-run: \`roll loop now\`
|
|
2876
|
+
- Take over manually: \`\$roll-build ${story_id}\`
|
|
2877
|
+
- Reset and retry: \`roll loop reset\` then \`roll loop now\`
|
|
2878
|
+
EOF
|
|
2879
|
+
_notify "roll ⚠ CI Failed" "${story_id}: CI did not pass"
|
|
2880
|
+
return 1
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2693
2883
|
# Verify TCR rhythm after a story completes. Returns 0 if ok, 1 if no TCR commits.
|
|
2694
2884
|
# On failure: reverts story in BACKLOG.md to 📋 Todo and writes ALERT.
|
|
2695
2885
|
_loop_enforce_tcr() {
|
|
@@ -2722,6 +2912,7 @@ _loop_enforce_tcr() {
|
|
|
2722
2912
|
- Take over manually: \`\$roll-build ${story_id}\`
|
|
2723
2913
|
- Reset and retry: \`roll loop reset\` then \`roll loop now\`
|
|
2724
2914
|
EOF
|
|
2915
|
+
_notify "roll ⚠ TCR Failed" "${story_id}: no tcr: commits found"
|
|
2725
2916
|
return 1
|
|
2726
2917
|
fi
|
|
2727
2918
|
|
|
@@ -2802,7 +2993,7 @@ _loop_monitor() {
|
|
|
2802
2993
|
# Alert
|
|
2803
2994
|
if [[ -f "$_LOOP_ALERT" ]]; then
|
|
2804
2995
|
echo ""
|
|
2805
|
-
echo -e " ${RED}⚠ ALERT
|
|
2996
|
+
echo -e " ${RED}⚠ ALERT${NC} (${CYAN}roll alert${NC} to manage)"
|
|
2806
2997
|
sed 's/^/ /' "$_LOOP_ALERT"
|
|
2807
2998
|
fi
|
|
2808
2999
|
|
|
@@ -2973,6 +3164,88 @@ _backlog_extract_id() {
|
|
|
2973
3164
|
fi
|
|
2974
3165
|
}
|
|
2975
3166
|
|
|
3167
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3168
|
+
# CI — check or wait for current commit's CI status
|
|
3169
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3170
|
+
# ALERT — view / ack / resolve loop alert lifecycle
|
|
3171
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3172
|
+
|
|
3173
|
+
cmd_alert() {
|
|
3174
|
+
local subcmd="${1:-list}"
|
|
3175
|
+
shift || true
|
|
3176
|
+
|
|
3177
|
+
case "$subcmd" in
|
|
3178
|
+
list|"")
|
|
3179
|
+
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
3180
|
+
ok "No active alerts 暂无告警"
|
|
3181
|
+
return 0
|
|
3182
|
+
fi
|
|
3183
|
+
echo -e "${BOLD}Active Alert 当前告警${NC}"
|
|
3184
|
+
echo ""
|
|
3185
|
+
cat "$_LOOP_ALERT"
|
|
3186
|
+
echo ""
|
|
3187
|
+
echo -e " Run '${CYAN}roll alert ack${NC}' to acknowledge, '${CYAN}roll alert resolve${NC}' to clear."
|
|
3188
|
+
echo -e " 运行 'roll alert ack' 确认告警,'roll alert resolve' 清除告警。"
|
|
3189
|
+
;;
|
|
3190
|
+
ack)
|
|
3191
|
+
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
3192
|
+
warn "No active alerts to acknowledge 暂无待确认告警"
|
|
3193
|
+
return 0
|
|
3194
|
+
fi
|
|
3195
|
+
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
|
|
3196
|
+
{
|
|
3197
|
+
echo ""
|
|
3198
|
+
echo "**Acknowledged**: ${ts}"
|
|
3199
|
+
} >> "$_LOOP_ALERT"
|
|
3200
|
+
ok "Alert acknowledged at ${ts} 告警已确认"
|
|
3201
|
+
;;
|
|
3202
|
+
resolve|clear)
|
|
3203
|
+
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
3204
|
+
ok "No active alerts 暂无告警"
|
|
3205
|
+
return 0
|
|
3206
|
+
fi
|
|
3207
|
+
rm -f "$_LOOP_ALERT"
|
|
3208
|
+
ok "Alert resolved and cleared 告警已解决并清除"
|
|
3209
|
+
;;
|
|
3210
|
+
*)
|
|
3211
|
+
err "Unknown subcommand: $subcmd 未知子命令: $subcmd"
|
|
3212
|
+
echo " Usage: roll alert [list|ack|resolve]"
|
|
3213
|
+
echo " 用法: roll alert [list|ack|resolve]"
|
|
3214
|
+
return 1
|
|
3215
|
+
;;
|
|
3216
|
+
esac
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3220
|
+
|
|
3221
|
+
cmd_ci() {
|
|
3222
|
+
local wait_mode=false
|
|
3223
|
+
local timeout=300
|
|
3224
|
+
|
|
3225
|
+
while [[ $# -gt 0 ]]; do
|
|
3226
|
+
case "$1" in
|
|
3227
|
+
--wait) wait_mode=true; shift ;;
|
|
3228
|
+
--timeout=*) timeout="${1#*=}"; shift ;;
|
|
3229
|
+
*) err "Usage: roll ci [--wait] [--timeout=N] 用法: roll ci [--wait] [--timeout=N]"; exit 1 ;;
|
|
3230
|
+
esac
|
|
3231
|
+
done
|
|
3232
|
+
|
|
3233
|
+
if $wait_mode; then
|
|
3234
|
+
_ci_wait "$timeout"
|
|
3235
|
+
return
|
|
3236
|
+
fi
|
|
3237
|
+
|
|
3238
|
+
command -v gh &>/dev/null || { warn "gh not installed gh 未安装"; return 0; }
|
|
3239
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
|
|
3240
|
+
local runs
|
|
3241
|
+
runs=$(gh run list --commit "$commit" --json status,conclusion,name 2>/dev/null) || { warn "gh run list failed"; return 0; }
|
|
3242
|
+
if [[ -z "$runs" || "$runs" == "[]" ]]; then
|
|
3243
|
+
echo "No CI runs for $(git rev-parse --short HEAD) 当前提交无 CI 记录"
|
|
3244
|
+
return 0
|
|
3245
|
+
fi
|
|
3246
|
+
echo "$runs" | jq -r '.[] | "\(.name): \(.status)/\(.conclusion)"'
|
|
3247
|
+
}
|
|
3248
|
+
|
|
2976
3249
|
cmd_backlog() {
|
|
2977
3250
|
local backlog="BACKLOG.md"
|
|
2978
3251
|
if [[ ! -f "$backlog" ]]; then
|
|
@@ -3193,7 +3466,7 @@ _dashboard() {
|
|
|
3193
3466
|
else
|
|
3194
3467
|
echo -e " Loop ${YELLOW}○ off${NC} run: roll loop on"
|
|
3195
3468
|
fi
|
|
3196
|
-
[[ -f "$_LOOP_ALERT" ]] && echo -e " ${RED}⚠ ALERT — run: roll
|
|
3469
|
+
[[ -f "$_LOOP_ALERT" ]] && echo -e " ${RED}⚠ ALERT — run: roll alert${NC}"
|
|
3197
3470
|
|
|
3198
3471
|
# Backlog summary
|
|
3199
3472
|
if [[ -f "BACKLOG.md" ]]; then
|
|
@@ -3253,6 +3526,7 @@ usage() {
|
|
|
3253
3526
|
echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
|
|
3254
3527
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
3255
3528
|
echo " release [Publish] Sync changelog + version bump + npm publish 同步日志并发版"
|
|
3529
|
+
echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
|
|
3256
3530
|
echo ""
|
|
3257
3531
|
echo "Examples / 示例:"
|
|
3258
3532
|
echo " roll setup # New machine: first-time install 新机器首次安装"
|
|
@@ -3280,8 +3554,10 @@ main() {
|
|
|
3280
3554
|
loop) cmd_loop "$@" ;;
|
|
3281
3555
|
brief) cmd_brief "$@" ;;
|
|
3282
3556
|
backlog) cmd_backlog "$@" ;;
|
|
3557
|
+
alert) cmd_alert "$@" ;;
|
|
3283
3558
|
agent) cmd_agent "$@" ;;
|
|
3284
3559
|
release) cmd_release "$@" ;;
|
|
3560
|
+
ci) cmd_ci "$@" ;;
|
|
3285
3561
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
3286
3562
|
help|--help|-h) usage ;;
|
|
3287
3563
|
"") [[ -f "BACKLOG.md" ]] && _dashboard || { usage; _show_changelog; } ;;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.512.
|
|
3
|
+
"version": "2026.512.8",
|
|
4
4
|
"description": "Roll — Roll out features with AI agents",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "
|
|
6
|
+
"test": "bash tests/run.sh"
|
|
7
7
|
},
|
|
8
8
|
"keywords": [
|
|
9
9
|
"ai",
|
|
@@ -101,6 +101,19 @@ CHANGELOG 是给**使用者**看的,不是给维护者看的。一句话讲清
|
|
|
101
101
|
- **Fixed**: 多个 loop 实例不会再互相打架(重复触发自动跳过)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
❌ 说机制不说现象(Fix 类最常犯):
|
|
105
|
+
```
|
|
106
|
+
- **Fixed**: `roll loop runs` 过滤条件从完整路径改为 slug,历史记录不再因路径不匹配而消失
|
|
107
|
+
- **Fixed**: `roll-loop` skill 写入 `runs.jsonl` 时 project slug 计算方式明确,避免写成 bare basename
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
✅ 直接说用户看到了什么:
|
|
111
|
+
```
|
|
112
|
+
- **Fixed**: `roll loop runs` 不再报"当前项目尚无运行记录",历史记录正常显示
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Fix 类句式参考:`<命令/功能> 不再 <之前的坏现象>`,或 `<命令/功能> 现在 <正常表现>`。内部有几个 bug 导致这一个现象,合并成一条。
|
|
116
|
+
|
|
104
117
|
### 4. Section Header — Always `## Unreleased`
|
|
105
118
|
|
|
106
119
|
**⚠️ do NOT guess version numbers.** Only `scripts/release.sh` assigns concrete
|
|
@@ -152,11 +165,20 @@ version numbers like `v2026.511.8`. Just write to `## Unreleased`.
|
|
|
152
165
|
|
|
153
166
|
**Ordering**: Unreleased always at top. Below it, released versions in reverse chronological order.
|
|
154
167
|
|
|
155
|
-
### 6.
|
|
168
|
+
### 6. Stage Update
|
|
169
|
+
|
|
170
|
+
**Normal path (called from `$roll-build` or `$roll-fix`)**: stage only — the
|
|
171
|
+
caller's completion commit will pick up CHANGELOG.md.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git add CHANGELOG.md
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Standalone / manual path** (called outside a roll-build session): stage and commit.
|
|
156
178
|
|
|
157
179
|
```bash
|
|
158
180
|
git add CHANGELOG.md
|
|
159
|
-
git commit -m "
|
|
181
|
+
git commit -m "chore: sync changelog"
|
|
160
182
|
git push
|
|
161
183
|
```
|
|
162
184
|
|