@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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +6 -5
- package/bin/roll +691 -77
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +87 -4
- package/skills/roll-doc/SKILL.md +184 -0
- package/skills/roll-loop/SKILL.md +45 -5
package/bin/roll
CHANGED
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Roll — AI Agent Convention Manager
|
|
5
5
|
# Single source of truth for how all AI coding agents behave.
|
|
6
6
|
|
|
7
|
-
VERSION="2026.
|
|
7
|
+
VERSION="2026.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
|
|
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
|
|
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 "
|
|
2109
|
+
( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
|
|
2058
2110
|
else
|
|
2059
|
-
( cd "
|
|
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
|
-
|
|
2222
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2626
|
-
|
|
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
|
|
2704
|
-
# Returns 1 on CI failure or
|
|
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
|
-
|
|
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
|
|
2720
|
-
|
|
2721
|
-
return
|
|
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
|
-
|
|
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
|
|
3332
|
-
|
|
3333
|
-
|
|
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
|
-
|
|
3890
|
+
printf " Now: ${DIM:-}idle${NC}\n"
|
|
3336
3891
|
fi
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3383
|
-
local
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
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
|
-
|
|
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
|
|