@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/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.6"
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
- _WK_MERGE_SUMMARY+=("unchanged|BACKLOG.md")
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
- _WK_MERGE_SUMMARY+=("created|BACKLOG.md")
1069
+ _ROLL_MERGE_SUMMARY+=("created|BACKLOG.md")
1072
1070
  }
1073
1071
 
1074
1072
  _ensure_features_dir() {
1075
1073
  if [[ -d "$1" ]]; then
1076
- _WK_MERGE_SUMMARY+=("unchanged|docs/features/")
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
- _WK_MERGE_SUMMARY+=("created|docs/features/")
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
- *) err "Usage: roll loop <on|off|now|test|status|monitor|runs|attach|mute|unmute|pause|resume|reset>"; exit 1 ;;
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:${NC}"; sed 's/^/ /' "$_LOOP_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 built_csv skipped_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; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
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
- printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
2627
- "$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
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:${NC}"
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 loop status${NC}"
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.6",
3
+ "version": "2026.512.8",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
- "test": "find tests/unit tests/integration -name '*.bats' | sort | xargs ./tests/helpers/bats-core/bin/bats"
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. Commit Update
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 "docs: update changelog for release $(date +%Y.%m.%d)"
181
+ git commit -m "chore: sync changelog"
160
182
  git push
161
183
  ```
162
184