@seanyao/roll 2026.512.7 → 2026.513.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/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"
7
+ VERSION="2026.513.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"
@@ -1437,7 +1437,7 @@ _peer_auto_attach() {
1437
1437
  fi
1438
1438
  local launched=0
1439
1439
  if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
1440
- open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
1440
+ open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
1441
1441
  fi
1442
1442
  if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
1443
1443
  osascript \
@@ -1447,7 +1447,7 @@ _peer_auto_attach() {
1447
1447
  && launched=1 || true
1448
1448
  fi
1449
1449
  if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
1450
- open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
1450
+ open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
1451
1451
  fi
1452
1452
  if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
1453
1453
  osascript \
@@ -1666,6 +1666,8 @@ cmd_peer() {
1666
1666
  peer_session="roll-peer-${from_tool}-${to_tool}"
1667
1667
  if ! tmux has-session -t "$peer_session" 2>/dev/null; then
1668
1668
  tmux new-session -d -s "$peer_session" -x 200 -y 50
1669
+ fi
1670
+ if [ -z "$(tmux list-clients -t "$peer_session" 2>/dev/null)" ]; then
1669
1671
  _peer_auto_attach "$peer_session"
1670
1672
  fi
1671
1673
  fi
@@ -1707,7 +1709,7 @@ cmd_peer() {
1707
1709
 
1708
1710
  printf '%s\n' "$response" >> "$log_file"
1709
1711
 
1710
- local resolution
1712
+ local resolution=""
1711
1713
  resolution="$(_peer_parse_resolution "$response")"
1712
1714
 
1713
1715
  if [[ -z "$resolution" ]]; then
@@ -1732,7 +1734,7 @@ cmd_peer() {
1732
1734
  if [[ "$round" -ge 3 ]]; then
1733
1735
  warn "Max rounds reached. Escalating to user. 达到最大轮数,升级给用户。"
1734
1736
  else
1735
- info "Peer requests $resolution. Continue to round $((round + 1)). Peer 请求 $resolution,继续第 $((round + 1)) 轮。"
1737
+ info "Peer requests ${resolution}. Continue to round $((round + 1)). Peer 请求 ${resolution},继续第 $((round + 1)) 轮。"
1736
1738
  fi
1737
1739
  ;;
1738
1740
  ESCALATE|UNKNOWN)
@@ -2046,17 +2048,67 @@ _write_loop_runner_script() {
2046
2048
  # Use stream-json + formatter: --verbose alone does nothing in -p mode;
2047
2049
  # stream-json enables realtime streaming; loop-fmt.py humanizes the events.
2048
2050
  local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
2051
+ local roll_bin="${ROLL_PKG_DIR}/bin/roll"
2049
2052
  local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
2053
+ # US-AUTO-037: strip leading `cd "<path>" && ` (callers like
2054
+ # _install_launchd_plists prepend it). The runner now manages cwd itself
2055
+ # — pointing at the worktree when isolation succeeds, project_path otherwise.
2056
+ local claude_cmd; claude_cmd="${cmd_verbose#cd \"*\" && }"
2057
+ local slug; slug=$(_project_slug "$project_path")
2050
2058
  cat > "$inner_path" << INNER
2051
2059
  #!/bin/bash -l
2052
2060
  set -o pipefail
2053
2061
  export PATH="/opt/homebrew/bin:\$PATH"
2062
+ # FIX-031: inner-level LOCK (PID + start-ts) — outer runner.sh LOCK can be
2063
+ # bypassed (recovery / retry / direct invocation); this guards the actual
2064
+ # claude invocation so a second session can't run under the same project.
2065
+ INNER_LOCK="\$(dirname "\$0")/.INNER-LOCK-\$(basename "\$0" -inner.sh | sed 's/^run-//')"
2066
+ if [ -f "\$INNER_LOCK" ]; then
2067
+ _prev_pid=""; _prev_ts=""
2068
+ IFS=: read -r _prev_pid _prev_ts < "\$INNER_LOCK" 2>/dev/null || true
2069
+ _now=\$(date -u +%s)
2070
+ if [ -n "\$_prev_pid" ] && [ -n "\$_prev_ts" ] \\
2071
+ && kill -0 "\$_prev_pid" 2>/dev/null \\
2072
+ && [ "\$((_now - _prev_ts))" -lt 14400 ]; then
2073
+ echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] inner loop already running (PID \$_prev_pid), skipping"
2074
+ exit 0
2075
+ fi
2076
+ rm -f "\$INNER_LOCK"
2077
+ fi
2078
+ printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
2079
+ trap 'rm -f "\$INNER_LOCK"' EXIT
2080
+
2081
+ # US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
2082
+ # safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
2083
+ # bin/roll's top-level \`set -euo pipefail\` infects us, so disable -e (the
2084
+ # retry loop relies on tolerating non-zero exits) while keeping pipefail.
2085
+ source "${roll_bin}"
2086
+ set +e
2087
+
2088
+ # Pre-claude: try to create a per-cycle isolated worktree on origin/main.
2089
+ # On any failure (no remote, no main, etc.) fall back to running in the
2090
+ # project's main tree (degraded — no isolation, like pre-037 behavior).
2091
+ CYCLE_ID="\$(date -u +%Y%m%d-%H%M%S)-\$\$"
2092
+ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
2093
+ BRANCH="loop/cycle-\${CYCLE_ID}"
2094
+ _USE_WORKTREE=0
2095
+ cd "${project_path}" 2>/dev/null || true
2096
+ if _worktree_fetch_origin main \\
2097
+ && _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
2098
+ _USE_WORKTREE=1
2099
+ _worktree_submodule_init "\$WT" 2>/dev/null || true
2100
+ echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
2101
+ else
2102
+ echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; running in main tree (no isolation)"
2103
+ WT="${project_path}"
2104
+ fi
2105
+
2054
2106
  FMT="${fmt_script}"
2055
2107
  for _attempt in 1 2 3; do
2056
2108
  if [ -f "\$FMT" ]; then
2057
- ( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
2109
+ ( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
2058
2110
  else
2059
- ( cd "${project_path}" && ${cmd_verbose} )
2111
+ ( cd "\$WT" && ${claude_cmd} )
2060
2112
  fi
2061
2113
  _exit=\$?
2062
2114
  [ "\$_exit" -eq 0 ] && break
@@ -2065,10 +2117,25 @@ for _attempt in 1 2 3; do
2065
2117
  sleep 30
2066
2118
  fi
2067
2119
  done
2120
+
2121
+ # Post-claude: merge back if we used an isolated worktree.
2122
+ if [ "\$_USE_WORKTREE" = "1" ]; then
2123
+ if [ "\$_exit" -eq 0 ]; then
2124
+ if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
2125
+ _worktree_cleanup "\$WT" "\$BRANCH"
2126
+ echo "[loop] cycle \${CYCLE_ID}: merged and cleaned up"
2127
+ else
2128
+ _worktree_alert "cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT (branch \$BRANCH)"
2129
+ echo "[loop] cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT"
2130
+ fi
2131
+ else
2132
+ _worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
2133
+ echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
2134
+ fi
2135
+ fi
2068
2136
  INNER
2069
2137
  chmod +x "$inner_path"
2070
2138
 
2071
- local slug; slug=$(_project_slug "$project_path")
2072
2139
  cat > "$script_path" << SCRIPT
2073
2140
  #!/bin/bash -l
2074
2141
  # Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
@@ -2218,8 +2285,13 @@ _install_launchd_plists() {
2218
2285
  if [[ "$before" != "$after" ]]; then
2219
2286
  updated=$((updated + 1))
2220
2287
  if _launchd_is_loaded "$label"; then
2221
- launchctl unload "$plist" 2>/dev/null || true
2222
- launchctl load "$plist" 2>/dev/null || true
2288
+ # FIX-027: use bootout/bootstrap so we don't disturb the label's
2289
+ # enabled flag in the launchd overrides db (which legacy
2290
+ # unload/load no-`-w` wipes on macOS Sonoma+, causing
2291
+ # `roll loop status` to falsely report off after `roll update`).
2292
+ local uid; uid=$(id -u)
2293
+ launchctl bootout "gui/${uid}/${label}" 2>/dev/null || true
2294
+ launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
2223
2295
  fi
2224
2296
  fi
2225
2297
  done
@@ -2595,11 +2667,12 @@ _loop_runs_dur() {
2595
2667
  }
2596
2668
 
2597
2669
  # Format a single JSONL run record for display.
2670
+ # Reads _LOOP_RUNS_BACKLOG global for ID→description lookup (set by _loop_runs).
2598
2671
  _loop_runs_format_line() {
2599
- local line="$1" show_project="$2"
2672
+ local line="$1" show_project="$2" is_darwin="$3"
2600
2673
  command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
2601
2674
 
2602
- local ts status project tcr duration alerts run_id reason built_count built_csv skipped_count
2675
+ local ts status project tcr duration alerts run_id reason built_count skipped_count
2603
2676
  ts=$(jq -r '.ts // ""' <<<"$line")
2604
2677
  status=$(jq -r '.status // ""' <<<"$line")
2605
2678
  project=$(jq -r '.project // ""' <<<"$line")
@@ -2609,10 +2682,16 @@ _loop_runs_format_line() {
2609
2682
  run_id=$(jq -r '.run_id // ""' <<<"$line")
2610
2683
  reason=$(jq -r '.reason // ""' <<<"$line")
2611
2684
  built_count=$(jq -r '(.built // []) | length' <<<"$line")
2612
- built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
2613
2685
  skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
2614
2686
 
2615
- local hhmm; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
2687
+ local hhmm epoch=""
2688
+ if [[ "$is_darwin" == "1" ]]; then
2689
+ epoch=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$ts" "+%s" 2>/dev/null) || epoch=""
2690
+ [[ -n "$epoch" ]] && hhmm=$(date -j -f "%s" "$epoch" "+%H:%M" 2>/dev/null) || hhmm=""
2691
+ else
2692
+ hhmm=$(date -d "$ts" "+%H:%M" 2>/dev/null) || hhmm=""
2693
+ fi
2694
+ [[ -z "$hhmm" ]] && hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
2616
2695
  local prefix=""
2617
2696
  if [[ "$show_project" == "true" ]]; then
2618
2697
  prefix="[$(basename "$project")] "
@@ -2622,8 +2701,29 @@ _loop_runs_format_line() {
2622
2701
  built)
2623
2702
  local skipped_note=""
2624
2703
  [[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
2625
- printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
2626
- "$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
2704
+ local items_word; [[ "$built_count" -eq 1 ]] && items_word="item" || items_word="items"
2705
+ printf " %s %s✅ built %d %s (%d tcr%s, %s)\n" \
2706
+ "$hhmm" "$prefix" "$built_count" "$items_word" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
2707
+ local id desc
2708
+ while IFS= read -r id; do
2709
+ [[ -z "$id" ]] && continue
2710
+ desc=""
2711
+ if [[ -n "$_LOOP_RUNS_BACKLOG" ]]; then
2712
+ desc=$(printf "%s\n" "$_LOOP_RUNS_BACKLOG" | awk -F'|' -v id="$id" '
2713
+ NF >= 3 {
2714
+ cell = $2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", cell)
2715
+ if (cell == id || cell ~ "^\\[" id "\\]") {
2716
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3); print $3; exit
2717
+ }
2718
+ }')
2719
+ fi
2720
+ if [[ -n "$desc" ]]; then
2721
+ [[ ${#desc} -gt 72 ]] && desc="${desc:0:69}..."
2722
+ printf " • %-14s %s\n" "$id" "$desc"
2723
+ else
2724
+ printf " • %s\n" "$id"
2725
+ fi
2726
+ done < <(jq -r '(.built // []) | .[]' <<<"$line")
2627
2727
  ;;
2628
2728
  idle)
2629
2729
  printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
@@ -2676,10 +2776,17 @@ _loop_runs() {
2676
2776
  local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
2677
2777
  local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
2678
2778
 
2779
+ local _is_darwin=""
2780
+ [[ "$(uname)" == "Darwin" ]] && _is_darwin="1"
2781
+
2782
+ _LOOP_RUNS_BACKLOG=""
2783
+ [[ -f "$project_path/BACKLOG.md" ]] && _LOOP_RUNS_BACKLOG=$(cat "$project_path/BACKLOG.md")
2784
+
2679
2785
  while IFS= read -r line; do
2680
2786
  [[ -z "$line" ]] && continue
2681
- _loop_runs_format_line "$line" "$all_flag"
2787
+ _loop_runs_format_line "$line" "$all_flag" "$_is_darwin"
2682
2788
  done <<<"$recent"
2789
+ unset _LOOP_RUNS_BACKLOG
2683
2790
  }
2684
2791
 
2685
2792
  # Send a macOS system notification. No-op when muted, non-macOS, or osascript unavailable.
@@ -2699,9 +2806,27 @@ _loop_tcr_count() {
2699
2806
  | awk '/^[a-f0-9]+ tcr:/{n++} END{print n+0}'
2700
2807
  }
2701
2808
 
2809
+ # Parse origin remote URL → "owner/repo" for GitHub repos.
2810
+ # Returns non-zero if no origin, or origin is not github.com.
2811
+ # Decoupled from `gh` auto-detection so SSH config host rewrites don't break it.
2812
+ _gh_repo_slug() {
2813
+ local url
2814
+ url=$(git config --get remote.origin.url 2>/dev/null) || return 1
2815
+ case "$url" in
2816
+ git@github.com:*) url="${url#git@github.com:}" ;;
2817
+ ssh://git@github.com/*) url="${url#ssh://git@github.com/}" ;;
2818
+ https://github.com/*) url="${url#https://github.com/}" ;;
2819
+ http://github.com/*) url="${url#http://github.com/}" ;;
2820
+ *) return 1 ;;
2821
+ esac
2822
+ url="${url%.git}"
2823
+ [[ -z "$url" ]] && return 1
2824
+ printf "%s\n" "$url"
2825
+ }
2826
+
2702
2827
  # Poll gh run list until current commit's CI completes.
2703
- # Returns 0 on success or when gh is unavailable (graceful skip).
2704
- # Returns 1 on CI failure or timeout.
2828
+ # Returns 0 on success (or when gh binary missing graceful skip).
2829
+ # Returns 1 on CI failure, timeout, or any gh call failure.
2705
2830
  _ci_wait() {
2706
2831
  local timeout="${1:-300}"
2707
2832
  local interval=15
@@ -2712,13 +2837,20 @@ _ci_wait() {
2712
2837
  local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
2713
2838
  local short; short=$(git rev-parse --short HEAD 2>/dev/null)
2714
2839
 
2715
- ok "Waiting for CI on ${short} 等待 CI: ${short}"
2840
+ # Resolve owner/repo from git remote so we don't depend on gh's auto-detection,
2841
+ # which breaks when ~/.ssh/config rewrites `Host github.com` → IP address.
2842
+ local repo_slug; repo_slug=$(_gh_repo_slug) || {
2843
+ err "Cannot determine GitHub repo from origin remote 无法从 origin 推导 GitHub 仓库"
2844
+ return 1
2845
+ }
2846
+
2847
+ ok "Waiting for CI on ${short} (${repo_slug}) 等待 CI: ${short}"
2716
2848
 
2717
2849
  while (( elapsed < timeout )); do
2718
2850
  local runs
2719
- runs=$(gh run list --commit "$commit" --json status,conclusion 2>/dev/null) || {
2720
- warn "gh run list failed skipping CI gate"
2721
- return 0
2851
+ runs=$(gh -R "$repo_slug" run list --commit "$commit" --json status,conclusion 2>&1) || {
2852
+ err "gh run list failed for ${repo_slug}@${short}: ${runs} gh 调用失败"
2853
+ return 1
2722
2854
  }
2723
2855
 
2724
2856
  if [[ -z "$runs" || "$runs" == "[]" ]]; then
@@ -2754,6 +2886,46 @@ _ci_wait() {
2754
2886
  return 1
2755
2887
  }
2756
2888
 
2889
+ # Pre-run CI health check — call before picking up new stories.
2890
+ # Refuses to build on a red base (HEAD CI failed). Lenient on unknown states
2891
+ # (gh missing, repo unparseable, no runs yet) — the post-build _loop_enforce_ci
2892
+ # is the strict gate.
2893
+ # Returns 0: ok to proceed (green / pending / unknown / no gh).
2894
+ # Returns 1: HEAD CI is definitively red → ALERT written, do not build.
2895
+ _loop_precheck_ci() {
2896
+ command -v gh &>/dev/null || return 0
2897
+
2898
+ local commit; commit=$(git rev-parse HEAD 2>/dev/null) || return 0
2899
+ local slug; slug=$(_gh_repo_slug) || return 0
2900
+
2901
+ local runs
2902
+ runs=$(gh -R "$slug" run list --commit "$commit" --json conclusion 2>/dev/null) || return 0
2903
+ [[ -z "$runs" || "$runs" == "[]" ]] && return 0
2904
+
2905
+ local failed
2906
+ failed=$(echo "$runs" | jq -r '[.[] | select(.conclusion != null and .conclusion != "success" and .conclusion != "skipped")] | length' 2>/dev/null || echo "0")
2907
+
2908
+ if [[ "$failed" -gt 0 ]]; then
2909
+ local short; short=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
2910
+ err "Pre-run CI check: HEAD CI is red — refuse to build on broken base (${short}) HEAD CI 红,拒绝在破损的基础上构建"
2911
+ mkdir -p "$(dirname "$_LOOP_ALERT")"
2912
+ cat > "$_LOOP_ALERT" << EOF
2913
+ # ALERT — Pre-run CI check failed (red base)
2914
+
2915
+ **Time**: $(date '+%Y-%m-%d %H:%M')
2916
+ **Commit**: ${short}
2917
+ **Reason**: HEAD CI is red — loop refused to build on a broken base HEAD CI 红,loop 拒绝在破损的基础上构建
2918
+
2919
+ **Action required**:
2920
+ - Investigate and fix CI: \`gh -R $(_gh_repo_slug) run list --commit ${commit}\`
2921
+ - After fixing and pushing green commit: \`roll loop now\`
2922
+ EOF
2923
+ _notify "roll ⚠ CI red" "loop refused to build on broken base (${short})"
2924
+ return 1
2925
+ fi
2926
+ return 0
2927
+ }
2928
+
2757
2929
  # CI gate before marking a story Done.
2758
2930
  # On CI failure: writes ALERT, returns 1 (caller keeps story 🔨 In Progress).
2759
2931
  # When gh unavailable: returns 0 (graceful skip).
@@ -2819,6 +2991,165 @@ EOF
2819
2991
  return 0
2820
2992
  }
2821
2993
 
2994
+ # FIX-032: dependency gate — parses BACKLOG inline tags so the loop SKILL
2995
+ # can enforce them at Step 2 (story pickup). Pure functions, no side effects.
2996
+ #
2997
+ # BACKLOG row format (relevant fragments):
2998
+ # | [US-AUTO-033](...) | desc `depends-on:US-AUTO-037` `manual-only:true` | 📋 Todo |
2999
+ # | FIX-100 | desc `depends-on:US-A,US-B` | 📋 Todo |
3000
+ #
3001
+ # Row matching is anchored on `^| [?<id>\b` so a story-id appearing in some
3002
+ # other row's depends-on list is not mistaken for the row that defines it.
3003
+
3004
+ # _loop_check_depends_on <story-id> [backlog-path]
3005
+ # Exit 0: all listed depends-on are ✅ Done, or no depends-on tag present.
3006
+ # Exit 1: any dep not ✅ Done, story-id not found, or backlog missing.
3007
+ # Stdout (on exit 1 due to unsatisfied deps): space-separated unsatisfied IDs.
3008
+ _loop_check_depends_on() {
3009
+ local id="$1"
3010
+ local backlog="${2:-BACKLOG.md}"
3011
+ [ -n "$id" ] || return 1
3012
+ [ -f "$backlog" ] || return 1
3013
+
3014
+ local row
3015
+ row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
3016
+ [ -n "$row" ] || return 1
3017
+
3018
+ local deps
3019
+ deps=$(echo "$row" | grep -oE 'depends-on:[A-Z][A-Z0-9,-]+' | head -1 | sed 's/depends-on://')
3020
+ [ -n "$deps" ] || return 0
3021
+
3022
+ local unsatisfied=""
3023
+ local dep
3024
+ local IFS_save="$IFS"
3025
+ IFS=','
3026
+ for dep in $deps; do
3027
+ local dep_row
3028
+ dep_row=$(grep -E "^\| \[?${dep}[]| ]" "$backlog" | head -1)
3029
+ if [ -z "$dep_row" ] || ! echo "$dep_row" | grep -qF '✅ Done'; then
3030
+ unsatisfied="${unsatisfied:+$unsatisfied }${dep}"
3031
+ fi
3032
+ done
3033
+ IFS="$IFS_save"
3034
+
3035
+ if [ -n "$unsatisfied" ]; then
3036
+ echo "$unsatisfied"
3037
+ return 1
3038
+ fi
3039
+ return 0
3040
+ }
3041
+
3042
+ # _loop_is_manual_only <story-id> [backlog-path]
3043
+ # Exit 0: story's own row carries `manual-only:true`.
3044
+ # Exit 1: tag absent, story-id not found, or backlog missing.
3045
+ _loop_is_manual_only() {
3046
+ local id="$1"
3047
+ local backlog="${2:-BACKLOG.md}"
3048
+ [ -n "$id" ] || return 1
3049
+ [ -f "$backlog" ] || return 1
3050
+
3051
+ local row
3052
+ row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
3053
+ [ -n "$row" ] || return 1
3054
+
3055
+ echo "$row" | grep -qE 'manual-only:true'
3056
+ }
3057
+
3058
+ # US-AUTO-036: worktree helpers (loop-safe pure additions).
3059
+ #
3060
+ # Phase 1 of worktree isolation — these helpers are NOT yet called by
3061
+ # runner.sh. US-AUTO-037 (manual-only) wires them into
3062
+ # _write_loop_runner_script. Do not delete or inline; they are unit-tested
3063
+ # in tests/unit/roll_worktree.bats.
3064
+
3065
+ # _worktree_path <slug> <us-id>
3066
+ # Echoes the canonical worktree directory for a (project, story) pair.
3067
+ _worktree_path() {
3068
+ echo "${_SHARED_ROOT}/worktrees/${1}-${2}"
3069
+ }
3070
+
3071
+ # _worktree_alert <msg>
3072
+ # Append a timestamped line to $_LOOP_ALERT. Used by failure paths in
3073
+ # _worktree_merge_back to surface stuck worktrees.
3074
+ _worktree_alert() {
3075
+ mkdir -p "$(dirname "$_LOOP_ALERT")" 2>/dev/null
3076
+ printf '[%s] worktree: %s\n' "$(date -u +%FT%TZ)" "$1" >> "$_LOOP_ALERT"
3077
+ }
3078
+
3079
+ # _worktree_create <path> <branch> <base>
3080
+ # Create a worktree at <path> on a new branch <branch> rooted at <base>.
3081
+ # Idempotent: if <branch> already exists locally (from a prior failed
3082
+ # run) it is deleted first so `git worktree add -b` does not error.
3083
+ _worktree_create() {
3084
+ local path="$1" branch="$2" base="$3"
3085
+ mkdir -p "$(dirname "$path")"
3086
+ if [ -e "$path" ]; then
3087
+ git worktree remove --force "$path" 2>/dev/null || true
3088
+ rm -rf "$path" 2>/dev/null || true
3089
+ fi
3090
+ if git show-ref --verify --quiet "refs/heads/${branch}"; then
3091
+ git branch -D "$branch" >/dev/null 2>&1 || true
3092
+ fi
3093
+ git worktree add "$path" -b "$branch" "$base"
3094
+ }
3095
+
3096
+ # _worktree_cleanup <path> <branch>
3097
+ # Remove the worktree at <path> and delete <branch>. Tolerant when
3098
+ # either is already absent so retries / partial-failure rollback is safe.
3099
+ _worktree_cleanup() {
3100
+ local path="$1" branch="$2"
3101
+ git worktree remove --force "$path" 2>/dev/null || true
3102
+ rm -rf "$path" 2>/dev/null || true
3103
+ git branch -D "$branch" 2>/dev/null || true
3104
+ return 0
3105
+ }
3106
+
3107
+ # _worktree_fetch_origin <branch>
3108
+ # `git fetch origin <branch>` quietly. Lenient on failure: a missing
3109
+ # remote / network blip must not derail the loop, so we return 0 even
3110
+ # when fetch fails (the loop's later ff-only check is the strict gate).
3111
+ _worktree_fetch_origin() {
3112
+ local branch="$1"
3113
+ if ! git fetch origin "$branch" --quiet 2>/dev/null; then
3114
+ echo "[worktree] fetch origin ${branch} failed (lenient, continuing)" >&2
3115
+ fi
3116
+ return 0
3117
+ }
3118
+
3119
+ # _worktree_submodule_init <path>
3120
+ # Run `git submodule update --init --recursive` inside the worktree at
3121
+ # <path> so its working tree is materially complete. Runs in a subshell
3122
+ # (cd is local) so the caller's cwd and the parent worktree's submodule
3123
+ # state are untouched. Returns submodule update's exit code.
3124
+ _worktree_submodule_init() {
3125
+ local path="$1"
3126
+ ( cd "$path" && git submodule update --init --recursive --quiet )
3127
+ }
3128
+
3129
+ # _worktree_merge_back <branch>
3130
+ # Caller must be in the main worktree (cwd = main). Steps:
3131
+ # 1. git pull --ff-only origin main (sync local main with remote)
3132
+ # 2. git merge --ff-only <branch> (linear merge of loop branch)
3133
+ # 3. git push origin main (publish)
3134
+ # Any failure → write to $_LOOP_ALERT and return 1 (worktree is left
3135
+ # in place by the caller for human inspection, per US-AUTO-036 non-goal).
3136
+ _worktree_merge_back() {
3137
+ local branch="$1"
3138
+ if ! git pull --ff-only origin main --quiet 2>/dev/null; then
3139
+ _worktree_alert "pull --ff-only origin main failed (remote diverged?)"
3140
+ return 1
3141
+ fi
3142
+ if ! git merge --ff-only "$branch" --quiet 2>/dev/null; then
3143
+ _worktree_alert "merge --ff-only ${branch} failed (not fast-forwardable from main)"
3144
+ return 1
3145
+ fi
3146
+ if ! git push origin main --quiet 2>/dev/null; then
3147
+ _worktree_alert "push origin main failed after merging ${branch}"
3148
+ return 1
3149
+ fi
3150
+ return 0
3151
+ }
3152
+
2822
3153
  _loop_monitor() {
2823
3154
  local interval="${1:-3}"
2824
3155
  local project_path; project_path=$(pwd -P)
@@ -3317,82 +3648,365 @@ cmd_backlog() {
3317
3648
  }
3318
3649
 
3319
3650
  # ─────────────────────────────────────────────────────────────────────────────
3651
+ # DASHBOARD — 自治优先六块布局 (US-AUTO-029)
3652
+ # ─────────────────────────────────────────────────────────────────────────────
3653
+
3654
+ # ① Identity — git working tree state.
3655
+ _dash_git_status() {
3656
+ git rev-parse --is-inside-work-tree &>/dev/null || { echo "—"; return; }
3657
+ if [[ -z "$(git status --porcelain 2>/dev/null)" ]]; then
3658
+ echo "✓"
3659
+ else
3660
+ echo "dirty"
3661
+ fi
3662
+ }
3663
+
3664
+ # ② Loop layer: extract in-progress story id|title|feature-link from BACKLOG.md.
3665
+ # Output empty if no row's *status column* is 🔨 In Progress (substring matches
3666
+ # anywhere on the row would catch description text that mentions the emoji).
3667
+ _dash_in_progress_story() {
3668
+ [[ -f "BACKLOG.md" ]] || return 0
3669
+ local row
3670
+ row=$(grep -F '| 🔨 In Progress |' BACKLOG.md | head -1) || return 0
3671
+ [[ -z "$row" ]] && return 0
3672
+ local id desc
3673
+ id=$(echo "$row" | grep -oE '(US|FIX|REFACTOR)-[A-Z]*-?[0-9]+' | head -1)
3674
+ desc=$(echo "$row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-60)
3675
+ local link
3676
+ link=$(echo "$row" | grep -oE 'docs/features/[^)]+' | head -1 || true)
3677
+ printf '%s|%s|%s' "$id" "$desc" "$link"
3678
+ }
3679
+
3680
+ # ② Loop layer: minutes since last "tcr:" commit, or empty if none.
3681
+ _dash_last_tcr_minutes() {
3682
+ git rev-parse --is-inside-work-tree &>/dev/null || return 0
3683
+ local last_ts
3684
+ last_ts=$(git log --grep='^tcr:' -1 --format=%ct 2>/dev/null)
3685
+ [[ -z "$last_ts" ]] && return 0
3686
+ local now; now=$(date +%s)
3687
+ echo $(( (now - last_ts) / 60 ))
3688
+ }
3689
+
3690
+ # ② Loop layer: tcr: commits since midnight today.
3691
+ _dash_tcr_today_count() {
3692
+ git rev-parse --is-inside-work-tree &>/dev/null || { echo 0; return; }
3693
+ local since; since=$(date '+%Y-%m-%d 00:00:00')
3694
+ git log --since="$since" --grep='^tcr:' --oneline 2>/dev/null | grep -c '^' || echo 0
3695
+ }
3696
+
3697
+ # ② Dream layer: hours since last dream log entry on disk.
3698
+ _dash_last_dream_hours() {
3699
+ local dream_log="${HOME}/.shared/roll/dream/log.md"
3700
+ [[ -f "$dream_log" ]] || return 0
3701
+ local mod_time now
3702
+ mod_time=$(stat -c %Y "$dream_log" 2>/dev/null || stat -f %m "$dream_log" 2>/dev/null || echo 0)
3703
+ now=$(date +%s)
3704
+ echo $(( (now - mod_time) / 3600 ))
3705
+ }
3706
+
3707
+ # ② Dream layer: count of REFACTOR-XXX rows currently 📋 Todo in BACKLOG.
3708
+ _dash_refactor_pending() {
3709
+ [[ -f "BACKLOG.md" ]] || { echo 0; return; }
3710
+ grep -E '^\| REFACTOR-' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' '
3711
+ }
3712
+
3713
+ # ② Peer layer: last result + days ago from peer log, empty if no log.
3714
+ _dash_last_peer() {
3715
+ local peer_log_dir="${HOME}/.shared/roll/peer"
3716
+ local latest
3717
+ latest=$(ls "$peer_log_dir"/*.log 2>/dev/null | sort | tail -1 || true)
3718
+ [[ -z "$latest" || ! -f "$latest" ]] && return 0
3719
+ local result
3720
+ result=$(grep -oE '(AGREE|REFINE|OBJECT|ESCALATE)' "$latest" 2>/dev/null | tail -1 || true)
3721
+ local mod_time now days
3722
+ mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
3723
+ now=$(date +%s)
3724
+ days=$(( (now - mod_time) / 86400 ))
3725
+ printf '%s|%s' "${result:-—}" "${days}"
3726
+ }
3727
+
3728
+ # ③ Pipeline counts → Idea Backlog Build (Verify/Release reserved).
3729
+ _dash_pipeline_counts() {
3730
+ [[ -f "BACKLOG.md" ]] || { echo "0 0 0 0 0"; return; }
3731
+ local idea backlog build
3732
+ idea=$(grep -E '^\| IDEA-' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
3733
+ backlog=$(grep -E '^\| (\[?US-|FIX-|REFACTOR-)' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
3734
+ build=$(grep -F '| 🔨 In Progress |' BACKLOG.md 2>/dev/null | wc -l | tr -d ' ')
3735
+ printf '%s %s %s 0 0' "$idea" "$backlog" "$build"
3736
+ }
3737
+
3738
+ # ④ DoD AC signal — read [x]/total checkboxes for a US section in feature doc.
3739
+ # Echoes "x/total"; "0/0" if no checkboxes found.
3740
+ _dash_ac_completion() {
3741
+ local feature_link="$1"
3742
+ [[ -z "$feature_link" ]] && { echo "0/0"; return; }
3743
+ local path="${feature_link%%#*}"
3744
+ local anchor="${feature_link##*#}"
3745
+ [[ ! -f "$path" ]] && { echo "0/0"; return; }
3746
+ # Extract the section from <a id="anchor"></a> or ## heading to next ## heading.
3747
+ local section
3748
+ section=$(awk -v anc="$anchor" '
3749
+ BEGIN{in_sec=0}
3750
+ /^<a id="/{
3751
+ gsub(/<a id="|"><\/a>/, "")
3752
+ if ($0 == anc) { in_sec=1; next }
3753
+ }
3754
+ in_sec && /^## /{ if(!started){ started=1; next } else { exit } }
3755
+ in_sec && started { print }
3756
+ in_sec { started_default=1 }
3757
+ ' "$path" 2>/dev/null)
3758
+ [[ -z "$section" ]] && {
3759
+ # Fallback: match heading line containing the anchor pattern directly.
3760
+ section=$(awk -v pat="$anchor" 'BEGIN{IGNORECASE=1}
3761
+ tolower($0) ~ pat && /^## /{p=1;next}
3762
+ p && /^## /{exit}
3763
+ p{print}' "$path" 2>/dev/null)
3764
+ }
3765
+ local done total
3766
+ done=$(echo "$section" | grep -cE '\[x\]' || echo 0)
3767
+ total=$(echo "$section" | grep -cE '\[[ x]\]' || echo 0)
3768
+ printf '%s/%s' "$done" "$total"
3769
+ }
3770
+
3771
+ # ④ DoD CI signal — query gh for HEAD's most-recent run conclusion.
3772
+ # Returns: success | pending | failure | none
3773
+ _dash_ci_status() {
3774
+ command -v gh &>/dev/null || { echo "none"; return; }
3775
+ local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { echo "none"; return; }
3776
+ local slug; slug=$(_gh_repo_slug 2>/dev/null) || true
3777
+ local out
3778
+ if [[ -n "$slug" ]]; then
3779
+ out=$(gh -R "$slug" run list --commit "$commit" --json status,conclusion 2>/dev/null) || { echo "none"; return; }
3780
+ else
3781
+ out=$(gh run list --commit "$commit" --json status,conclusion 2>/dev/null) || { echo "none"; return; }
3782
+ fi
3783
+ [[ -z "$out" || "$out" == "[]" ]] && { echo "none"; return; }
3784
+ local concl status
3785
+ concl=$(echo "$out" | jq -r '.[0].conclusion // ""' 2>/dev/null)
3786
+ status=$(echo "$out" | jq -r '.[0].status // ""' 2>/dev/null)
3787
+ if [[ "$status" == "in_progress" || "$status" == "queued" ]]; then
3788
+ echo "pending"
3789
+ elif [[ "$concl" == "success" ]]; then
3790
+ echo "success"
3791
+ elif [[ -n "$concl" ]]; then
3792
+ echo "failure"
3793
+ else
3794
+ echo "pending"
3795
+ fi
3796
+ }
3797
+
3798
+ # ⑤ Active ALERT count (number of "# ALERT" headings in ALERT.md, 0 if absent).
3799
+ _dash_alert_count() {
3800
+ [[ -f "$_LOOP_ALERT" ]] || { echo 0; return; }
3801
+ grep '^# ALERT' "$_LOOP_ALERT" 2>/dev/null | wc -l | tr -d ' '
3802
+ }
3803
+
3804
+ # ⑤ Pending proposal count — "## PROPOSAL:" entries in PROPOSALS.md.
3805
+ _dash_proposal_count() {
3806
+ [[ -f "PROPOSALS.md" ]] || { echo 0; return; }
3807
+ grep '^## PROPOSAL' PROPOSALS.md 2>/dev/null | wc -l | tr -d ' '
3808
+ }
3809
+
3810
+ # ⑤ Release-ready signal — true if latest brief contains 可发版 or "Release ready".
3811
+ _dash_release_ready() {
3812
+ local latest
3813
+ latest=$(ls docs/briefs/*.md 2>/dev/null | sort | tail -1 || true)
3814
+ [[ -z "$latest" ]] && return 1
3815
+ grep -qE '✅ 可发版|Release ready' "$latest" 2>/dev/null
3816
+ }
3817
+
3818
+ # ⑥ Latest brief summary — first non-trivial line after frontmatter.
3819
+ _dash_brief_summary() {
3820
+ local latest="$1"
3821
+ [[ -z "$latest" || ! -f "$latest" ]] && return 0
3822
+ awk '
3823
+ NR==1 && /^#/ { next } # skip H1 title
3824
+ /^>/ { next } # skip blockquote
3825
+ /^---$/ { next }
3826
+ /^$/ { next }
3827
+ /^## /{ gsub(/^## */,""); print; exit }
3828
+ /^[^[:space:]]/{ print; exit }
3829
+ ' "$latest" 2>/dev/null | head -1 | cut -c1-60
3830
+ }
3320
3831
 
3321
3832
  _dashboard() {
3322
3833
  local project_path; project_path=$(pwd -P)
3323
3834
  local project_name; project_name=$(basename "$project_path")
3324
3835
  local agent; agent=$(_project_agent)
3836
+ local git_state; git_state=$(_dash_git_status)
3837
+ local is_darwin=false
3838
+ [[ "$(uname)" == "Darwin" ]] && is_darwin=true
3839
+
3840
+ # ── ① Identity ─────────────────────────────────────────────────────────────
3841
+ echo ""
3842
+ printf " ${BOLD}${CYAN}%s${NC} ${YELLOW}v%s${NC} ${BOLD}·${NC} agent ${CYAN}%s${NC} ${BOLD}·${NC} git " \
3843
+ "$project_name" "$VERSION" "$agent"
3844
+ case "$git_state" in
3845
+ ✓) printf "${GREEN}✓${NC}\n" ;;
3846
+ dirty) printf "${YELLOW}dirty${NC}\n" ;;
3847
+ *) printf "${YELLOW}%s${NC}\n" "$git_state" ;;
3848
+ esac
3849
+ echo ""
3325
3850
 
3326
- echo -e "\n ${BOLD}${CYAN}${project_name}${NC} ${YELLOW}v${VERSION}${NC}\n"
3851
+ # ── ② AI 自治 — 三层 × 四道防线 (主视觉) ────────────────────────────────
3852
+ echo -e " ${BOLD}╔══ 🤖 AI 自治 — 三层 × 四道防线 ══════════════════════════╗${NC}"
3327
3853
 
3854
+ # Loop layer
3855
+ local loop_state="not-installed"
3328
3856
  local _dash_loop_paused=false
3329
3857
  [[ -f "$_LOOP_STATE" ]] && grep -q "^status: paused" "$_LOOP_STATE" 2>/dev/null && _dash_loop_paused=true
3858
+ if $is_darwin; then
3859
+ loop_state=$(_launchd_svc_state "loop" "$project_path")
3860
+ else
3861
+ crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_state="enabled"
3862
+ fi
3863
+ local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
3864
+ active_start=$(_config_read_int "loop_active_start" "10")
3865
+ active_end=$(_config_read_int "loop_active_end" "18")
3866
+ loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
3867
+ dream_hour=$(_config_read_int "loop_dream_hour" "3")
3868
+ dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
3869
+ brief_hour=$(_config_read_int "loop_brief_hour" "9")
3870
+ brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
3330
3871
 
3331
- local _dash_loop_enabled=false
3332
- if [[ "$(uname)" == "Darwin" ]]; then
3333
- _launchd_is_loaded "$(_launchd_label "loop" "$project_path")" && _dash_loop_enabled=true
3872
+ local loop_badge loop_sched
3873
+ loop_sched=$(printf "every :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
3874
+ case "$loop_state" in
3875
+ enabled) loop_badge="${GREEN}● enabled${NC}" ;;
3876
+ installed-off) loop_badge="${YELLOW}⚠ off${NC}" ;;
3877
+ *) loop_badge="${RED}○ missing${NC}" ;;
3878
+ esac
3879
+ $_dash_loop_paused && loop_badge="${YELLOW}⏸ paused${NC}"
3880
+ printf " Loop Layer %b %s\n" "$loop_badge" "$loop_sched"
3881
+
3882
+ # Loop "Now:" line — current in-progress story, if any.
3883
+ local in_prog; in_prog=$(_dash_in_progress_story)
3884
+ if [[ -n "$in_prog" ]]; then
3885
+ local p_id p_desc
3886
+ p_id=${in_prog%%|*}
3887
+ p_desc=$(echo "$in_prog" | awk -F'|' '{print $2}')
3888
+ printf " Now: ${BOLD}🔨 %s${NC} %s\n" "$p_id" "$p_desc"
3334
3889
  else
3335
- crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && _dash_loop_enabled=true
3890
+ printf " Now: ${DIM:-}idle${NC}\n"
3336
3891
  fi
3337
- if $_dash_loop_paused; then
3338
- echo -e " Loop ${YELLOW}⏸ paused${NC} run: roll loop resume"
3339
- elif $_dash_loop_enabled; then
3340
- echo -e " Loop ${GREEN}● on${NC} Agent: ${CYAN}${agent}${NC}"
3341
- if [[ "$(uname)" == "Darwin" ]]; then
3342
- local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
3343
- active_start=$(_config_read_int "loop_active_start" "10")
3344
- active_end=$(_config_read_int "loop_active_end" "18")
3345
- loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
3346
- dream_hour=$(_config_read_int "loop_dream_hour" "3")
3347
- dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
3348
- brief_hour=$(_config_read_int "loop_brief_hour" "9")
3349
- brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
3350
- local loop_sched dream_sched brief_sched
3351
- loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
3352
- dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
3353
- brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
3354
- local svcs=("loop" "dream" "brief")
3355
- local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
3356
- for i in "${!svcs[@]}"; do
3357
- local svc="${svcs[$i]}" schedule="${scheds[$i]}"
3358
- local state; state=$(_launchd_svc_state "$svc" "$project_path")
3359
- case "$state" in
3360
- enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
3361
- installed-off) printf " ${YELLOW}%-8s ⚠ off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
3362
- not-installed) printf " ${RED}%-8s ○ missing${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
3363
- esac
3364
- done
3365
- fi
3892
+
3893
+ # last TCR + today count
3894
+ local last_tcr_min today_tcr
3895
+ last_tcr_min=$(_dash_last_tcr_minutes)
3896
+ today_tcr=$(_dash_tcr_today_count)
3897
+ if [[ -n "$last_tcr_min" ]]; then
3898
+ printf " last TCR ${CYAN}%smin${NC} ago · ${CYAN}%s${NC} micro-commits today\n" "$last_tcr_min" "$today_tcr"
3366
3899
  else
3367
- echo -e " Loop ${YELLOW}○ off${NC} run: roll loop on"
3900
+ printf " no tcr commits yet\n"
3368
3901
  fi
3369
- [[ -f "$_LOOP_ALERT" ]] && echo -e " ${RED}⚠ ALERT — run: roll alert${NC}"
3370
3902
 
3371
- # Backlog summary
3372
- if [[ -f "BACKLOG.md" ]]; then
3373
- local pending_count
3374
- pending_count=$(grep -cF '| 📋 Todo |' "BACKLOG.md" 2>/dev/null) || pending_count=0
3375
- if (( pending_count > 0 )); then
3376
- echo -e " Backlog ${YELLOW}${pending_count} pending${NC} run: roll backlog"
3903
+ # Dream layer
3904
+ local dream_state="not-installed"
3905
+ $is_darwin && dream_state=$(_launchd_svc_state "dream" "$project_path")
3906
+ local dream_badge
3907
+ case "$dream_state" in
3908
+ enabled) dream_badge="${GREEN} enabled${NC}" ;;
3909
+ installed-off) dream_badge="${YELLOW}⚠ off${NC}" ;;
3910
+ *) dream_badge="${RED}○ missing${NC}" ;;
3911
+ esac
3912
+ printf " Dream Layer %b %02d:%02d\n" "$dream_badge" "$dream_hour" "$dream_minute"
3913
+ local dream_hours refac_pending
3914
+ dream_hours=$(_dash_last_dream_hours)
3915
+ refac_pending=$(_dash_refactor_pending)
3916
+ if [[ -n "$dream_hours" ]]; then
3917
+ printf " Last scan ${CYAN}%sh${NC} ago → ${CYAN}%s${NC} REFACTOR queued\n" "$dream_hours" "$refac_pending"
3918
+ else
3919
+ printf " no scan yet → ${CYAN}%s${NC} REFACTOR queued\n" "$refac_pending"
3920
+ fi
3921
+
3922
+ # Peer layer
3923
+ local peer; peer=$(_dash_last_peer)
3924
+ printf " Peer Layer ${GREEN}● ready${NC} on complexity=large\n"
3925
+ if [[ -n "$peer" ]]; then
3926
+ local peer_res peer_days
3927
+ peer_res=${peer%%|*}
3928
+ peer_days=${peer##*|}
3929
+ printf " Last call ${CYAN}%sd${NC} ago · %s\n" "$peer_days" "$peer_res"
3930
+ else
3931
+ printf " Last call —\n"
3932
+ fi
3933
+
3934
+ # 四道防线
3935
+ echo -e " ${BOLD}─ 四道防线 ─${NC}"
3936
+ local def_tcr="${RED}○${NC}" def_review="${GREEN}●${NC}" def_spar="${YELLOW}○${NC}" def_sentinel="${YELLOW}○ off${NC}"
3937
+ if [[ -n "$last_tcr_min" ]]; then
3938
+ def_tcr="${GREEN}● ${last_tcr_min}min${NC}"
3939
+ fi
3940
+ printf " TCR %b Spar %b Auto Review %b Sentinel %b\n" \
3941
+ "$def_tcr" "$def_spar" "$def_review" "$def_sentinel"
3942
+ echo -e " ${BOLD}╚══════════════════════════════════════════════════════════╝${NC}"
3943
+ echo ""
3944
+
3945
+ # ── ③ Pipeline 全景 ────────────────────────────────────────────────────────
3946
+ read -r pl_idea pl_backlog pl_build pl_verify pl_release <<< "$(_dash_pipeline_counts)"
3947
+ local build_color="${DIM:-}"
3948
+ (( pl_build > 0 )) && build_color="${BOLD}${YELLOW}"
3949
+ printf " ${BOLD}📦 Pipeline${NC} Idea %s ▸ Backlog %s ▸ Build %b%s🔨${NC} ▸ Verify %s ▸ Release %s\n" \
3950
+ "$pl_idea" "$pl_backlog" "$build_color" "$pl_build" "$pl_verify" "$pl_release"
3951
+ echo ""
3952
+
3953
+ # ── ④ Current Focus · DoD (仅当 Build > 0) ──────────────────────────────
3954
+ if [[ -n "$in_prog" && "$pl_build" -gt 0 ]]; then
3955
+ local p_id p_desc p_link
3956
+ p_id=${in_prog%%|*}
3957
+ p_desc=$(echo "$in_prog" | awk -F'|' '{print $2}')
3958
+ p_link=$(echo "$in_prog" | awk -F'|' '{print $3}')
3959
+ local ac_ratio; ac_ratio=$(_dash_ac_completion "$p_link")
3960
+ local ac_done="${ac_ratio%%/*}" ac_total="${ac_ratio##*/}"
3961
+ local ac_badge ci_badge
3962
+ if [[ "$ac_total" != "0" && "$ac_done" == "$ac_total" ]]; then
3963
+ ac_badge="${GREEN}✓ AC${NC}"
3377
3964
  else
3378
- echo -e " Backlog ${GREEN} clear${NC}"
3965
+ ac_badge="${YELLOW} AC ${ac_done}/${ac_total}${NC}"
3379
3966
  fi
3967
+ local ci_state; ci_state=$(_dash_ci_status)
3968
+ case "$ci_state" in
3969
+ success) ci_badge="${GREEN}✓ CI${NC}" ;;
3970
+ pending) ci_badge="${YELLOW}… CI${NC}" ;;
3971
+ failure) ci_badge="${RED}✗ CI${NC}" ;;
3972
+ *) ci_badge="${YELLOW}○ CI${NC}" ;;
3973
+ esac
3974
+ printf " ${BOLD}📊 Current Focus · DoD${NC}\n"
3975
+ printf " 🔨 ${BOLD}%s${NC} %s\n" "$p_id" "$p_desc"
3976
+ printf " [%b] [%b]\n" "$ac_badge" "$ci_badge"
3977
+ printf " ${YELLOW}其余 4 项 DoD 信号源待接入:see US-AUTO-030/031, IDEA-013/014${NC}\n"
3978
+ echo ""
3380
3979
  fi
3381
3980
 
3382
- local briefs_dir="docs/briefs"
3383
- local latest; latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
3384
- if [[ -n "$latest" ]]; then
3385
- local mod_time now age
3386
- mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
3387
- now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
3388
- local readiness; readiness=$(awk '/^## Release Readiness/{found=1;next} found && /[^[:space:]]/{gsub(/\*\*/,""); print; exit}' "$latest" 2>/dev/null || true)
3389
- local done_count; done_count=$(grep -c '| Story\|| Fix\|| Refactor' "$latest" 2>/dev/null) || done_count=0
3390
- echo -e "\n Brief (${age}h ago) ${readiness:+${CYAN}${readiness}${NC}}"
3391
- [[ "$done_count" -gt 0 ]] && echo -e " This cycle: ${done_count} items completed"
3981
+ # ── ⑤ Human × AI 介入区 ───────────────────────────────────────────────────
3982
+ local alerts proposals release_ready
3983
+ alerts=$(_dash_alert_count); alerts=${alerts//[^0-9]/}; alerts=${alerts:-0}
3984
+ proposals=$(_dash_proposal_count); proposals=${proposals//[^0-9]/}; proposals=${proposals:-0}
3985
+ release_ready=false; _dash_release_ready && release_ready=true
3986
+ printf " ${BOLD}👤 需要你介入${NC}\n"
3987
+ if (( alerts == 0 )) && (( proposals == 0 )) && ! $release_ready; then
3988
+ printf " ${GREEN}✓ AI 自驱中 无需介入${NC}\n"
3392
3989
  else
3393
- echo -e "\n No brief yet run: roll brief"
3990
+ (( alerts > 0 )) && printf " ${RED}⚠ %s ALERT${NC} run: roll alert\n" "$alerts"
3991
+ (( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} run: roll backlog\n" "$proposals"
3992
+ $release_ready && printf " ${GREEN}✓ Release ready${NC} run: roll release\n"
3394
3993
  fi
3994
+ echo ""
3395
3995
 
3996
+ # ── ⑥ Schedules & Last Brief ──────────────────────────────────────────────
3997
+ printf " ${BOLD}⏰ Schedules & Last Brief${NC}\n"
3998
+ printf " loop :%02d · dream %02d:%02d · brief %02d:%02d\n" \
3999
+ "$loop_minute" "$dream_hour" "$dream_minute" "$brief_hour" "$brief_minute"
4000
+ local latest_brief; latest_brief=$(ls docs/briefs/*.md 2>/dev/null | sort | tail -1 || true)
4001
+ if [[ -n "$latest_brief" ]]; then
4002
+ local mod_time now age summary
4003
+ mod_time=$(stat -c %Y "$latest_brief" 2>/dev/null || stat -f %m "$latest_brief" 2>/dev/null || echo 0)
4004
+ now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
4005
+ summary=$(_dash_brief_summary "$latest_brief")
4006
+ printf " Brief ${CYAN}%sh${NC} ago — %s\n" "$age" "${summary:-—}"
4007
+ else
4008
+ printf " Brief: ${YELLOW}none yet${NC} — run: roll brief\n"
4009
+ fi
3396
4010
  echo ""
3397
4011
  }
3398
4012