@seanyao/roll 2026.512.8 → 2026.514.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 +29 -8
- package/LICENSE +21 -0
- package/bin/roll +903 -64
- package/conventions/global/AGENTS.md +6 -0
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +98 -2
- package/skills/roll-.dream/SKILL.md +2 -0
- package/skills/roll-build/SKILL.md +3 -1
- package/skills/roll-design/SKILL.md +2 -0
- package/skills/roll-loop/SKILL.md +19 -0
- package/skills/roll-peer/SKILL.md +27 -0
- package/skills/roll-propose/SKILL.md +4 -0
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.514.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"
|
|
@@ -609,6 +609,36 @@ cmd_setup() {
|
|
|
609
609
|
|
|
610
610
|
echo ""
|
|
611
611
|
info "Next: run ${BOLD}roll init${NC} inside a project to initialize it. 下一步:在项目目录运行 roll init"
|
|
612
|
+
|
|
613
|
+
echo ""
|
|
614
|
+
_print_pr_pipeline_hint
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# ─── PR pipeline hint ────────────────────────────────────────────────────────
|
|
618
|
+
# US-AUTO-035: print the one-time branch-protection command that flips repo
|
|
619
|
+
# from path A (CI gate only) to path C (CI + AI review double gate). Reading
|
|
620
|
+
# this hint is opt-in; the command is destructive (changes branch protection)
|
|
621
|
+
# so it is never run automatically.
|
|
622
|
+
_print_pr_pipeline_hint() {
|
|
623
|
+
cat <<'HINT'
|
|
624
|
+
|
|
625
|
+
Optional — enable AI review as a hard merge gate (path C).
|
|
626
|
+
可选 —— 启用 AI 评审作为合并双门(路径 C)。
|
|
627
|
+
|
|
628
|
+
Run once per repo (requires admin token), then claude-code-review.yml
|
|
629
|
+
approvals become a required merge gate alongside CI:
|
|
630
|
+
每个仓库执行一次(需要管理员 token),之后 claude-code-review.yml 的
|
|
631
|
+
approve 将与 CI 一起成为合并必经的双门:
|
|
632
|
+
|
|
633
|
+
gh api -X PATCH repos/<owner>/<repo>/branches/main/protection \
|
|
634
|
+
-f required_pull_request_reviews.required_approving_review_count=1
|
|
635
|
+
|
|
636
|
+
Escape hatch: add [skip-ai-review] to a PR body, or include
|
|
637
|
+
SKIP_AI_REVIEW in any commit message, to bypass AI review for that PR.
|
|
638
|
+
紧急通道:在 PR body 加 [skip-ai-review],或在任一 commit message
|
|
639
|
+
里包含 SKIP_AI_REVIEW,可对该 PR 绕过 AI 评审。
|
|
640
|
+
|
|
641
|
+
HINT
|
|
612
642
|
}
|
|
613
643
|
|
|
614
644
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1437,7 +1467,7 @@ _peer_auto_attach() {
|
|
|
1437
1467
|
fi
|
|
1438
1468
|
local launched=0
|
|
1439
1469
|
if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
|
|
1440
|
-
open -na Ghostty.app --args -e
|
|
1470
|
+
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1441
1471
|
fi
|
|
1442
1472
|
if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
|
|
1443
1473
|
osascript \
|
|
@@ -1447,7 +1477,7 @@ _peer_auto_attach() {
|
|
|
1447
1477
|
&& launched=1 || true
|
|
1448
1478
|
fi
|
|
1449
1479
|
if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
|
|
1450
|
-
open -na Ghostty.app --args -e
|
|
1480
|
+
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1451
1481
|
fi
|
|
1452
1482
|
if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
|
|
1453
1483
|
osascript \
|
|
@@ -1666,6 +1696,8 @@ cmd_peer() {
|
|
|
1666
1696
|
peer_session="roll-peer-${from_tool}-${to_tool}"
|
|
1667
1697
|
if ! tmux has-session -t "$peer_session" 2>/dev/null; then
|
|
1668
1698
|
tmux new-session -d -s "$peer_session" -x 200 -y 50
|
|
1699
|
+
fi
|
|
1700
|
+
if [ -z "$(tmux list-clients -t "$peer_session" 2>/dev/null)" ]; then
|
|
1669
1701
|
_peer_auto_attach "$peer_session"
|
|
1670
1702
|
fi
|
|
1671
1703
|
fi
|
|
@@ -1707,7 +1739,7 @@ cmd_peer() {
|
|
|
1707
1739
|
|
|
1708
1740
|
printf '%s\n' "$response" >> "$log_file"
|
|
1709
1741
|
|
|
1710
|
-
local resolution
|
|
1742
|
+
local resolution=""
|
|
1711
1743
|
resolution="$(_peer_parse_resolution "$response")"
|
|
1712
1744
|
|
|
1713
1745
|
if [[ -z "$resolution" ]]; then
|
|
@@ -1732,7 +1764,7 @@ cmd_peer() {
|
|
|
1732
1764
|
if [[ "$round" -ge 3 ]]; then
|
|
1733
1765
|
warn "Max rounds reached. Escalating to user. 达到最大轮数,升级给用户。"
|
|
1734
1766
|
else
|
|
1735
|
-
info "Peer requests $resolution. Continue to round $((round + 1)). Peer 请求 $resolution,继续第 $((round + 1)) 轮。"
|
|
1767
|
+
info "Peer requests ${resolution}. Continue to round $((round + 1)). Peer 请求 ${resolution},继续第 $((round + 1)) 轮。"
|
|
1736
1768
|
fi
|
|
1737
1769
|
;;
|
|
1738
1770
|
ESCALATE|UNKNOWN)
|
|
@@ -2046,17 +2078,67 @@ _write_loop_runner_script() {
|
|
|
2046
2078
|
# Use stream-json + formatter: --verbose alone does nothing in -p mode;
|
|
2047
2079
|
# stream-json enables realtime streaming; loop-fmt.py humanizes the events.
|
|
2048
2080
|
local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
|
|
2081
|
+
local roll_bin="${ROLL_PKG_DIR}/bin/roll"
|
|
2049
2082
|
local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
|
|
2083
|
+
# US-AUTO-037: strip leading `cd "<path>" && ` (callers like
|
|
2084
|
+
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
2085
|
+
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
2086
|
+
local claude_cmd; claude_cmd="${cmd_verbose#cd \"*\" && }"
|
|
2087
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2050
2088
|
cat > "$inner_path" << INNER
|
|
2051
2089
|
#!/bin/bash -l
|
|
2052
2090
|
set -o pipefail
|
|
2053
2091
|
export PATH="/opt/homebrew/bin:\$PATH"
|
|
2092
|
+
# FIX-031: inner-level LOCK (PID + start-ts) — outer runner.sh LOCK can be
|
|
2093
|
+
# bypassed (recovery / retry / direct invocation); this guards the actual
|
|
2094
|
+
# claude invocation so a second session can't run under the same project.
|
|
2095
|
+
INNER_LOCK="\$(dirname "\$0")/.INNER-LOCK-\$(basename "\$0" -inner.sh | sed 's/^run-//')"
|
|
2096
|
+
if [ -f "\$INNER_LOCK" ]; then
|
|
2097
|
+
_prev_pid=""; _prev_ts=""
|
|
2098
|
+
IFS=: read -r _prev_pid _prev_ts < "\$INNER_LOCK" 2>/dev/null || true
|
|
2099
|
+
_now=\$(date -u +%s)
|
|
2100
|
+
if [ -n "\$_prev_pid" ] && [ -n "\$_prev_ts" ] \\
|
|
2101
|
+
&& kill -0 "\$_prev_pid" 2>/dev/null \\
|
|
2102
|
+
&& [ "\$((_now - _prev_ts))" -lt 14400 ]; then
|
|
2103
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] inner loop already running (PID \$_prev_pid), skipping"
|
|
2104
|
+
exit 0
|
|
2105
|
+
fi
|
|
2106
|
+
rm -f "\$INNER_LOCK"
|
|
2107
|
+
fi
|
|
2108
|
+
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
2109
|
+
trap 'rm -f "\$INNER_LOCK"' EXIT
|
|
2110
|
+
|
|
2111
|
+
# US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
|
|
2112
|
+
# safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
|
|
2113
|
+
# bin/roll's top-level \`set -euo pipefail\` infects us, so disable -e (the
|
|
2114
|
+
# retry loop relies on tolerating non-zero exits) while keeping pipefail.
|
|
2115
|
+
source "${roll_bin}"
|
|
2116
|
+
set +e
|
|
2117
|
+
|
|
2118
|
+
# Pre-claude: try to create a per-cycle isolated worktree on origin/main.
|
|
2119
|
+
# On any failure (no remote, no main, etc.) fall back to running in the
|
|
2120
|
+
# project's main tree (degraded — no isolation, like pre-037 behavior).
|
|
2121
|
+
CYCLE_ID="\$(date -u +%Y%m%d-%H%M%S)-\$\$"
|
|
2122
|
+
WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
2123
|
+
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2124
|
+
_USE_WORKTREE=0
|
|
2125
|
+
cd "${project_path}" 2>/dev/null || true
|
|
2126
|
+
if _worktree_fetch_origin main \\
|
|
2127
|
+
&& _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
|
|
2128
|
+
_USE_WORKTREE=1
|
|
2129
|
+
_worktree_submodule_init "\$WT" 2>/dev/null || true
|
|
2130
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
2131
|
+
else
|
|
2132
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; running in main tree (no isolation)"
|
|
2133
|
+
WT="${project_path}"
|
|
2134
|
+
fi
|
|
2135
|
+
|
|
2054
2136
|
FMT="${fmt_script}"
|
|
2055
2137
|
for _attempt in 1 2 3; do
|
|
2056
2138
|
if [ -f "\$FMT" ]; then
|
|
2057
|
-
( cd "
|
|
2139
|
+
( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
|
|
2058
2140
|
else
|
|
2059
|
-
( cd "
|
|
2141
|
+
( cd "\$WT" && ${claude_cmd} )
|
|
2060
2142
|
fi
|
|
2061
2143
|
_exit=\$?
|
|
2062
2144
|
[ "\$_exit" -eq 0 ] && break
|
|
@@ -2065,10 +2147,41 @@ for _attempt in 1 2 3; do
|
|
|
2065
2147
|
sleep 30
|
|
2066
2148
|
fi
|
|
2067
2149
|
done
|
|
2150
|
+
|
|
2151
|
+
# Post-claude: publish cycle branch. Doc-only changes (BACKLOG/docs) merge
|
|
2152
|
+
# immediately via --admin; code changes use auto-merge (CI gate required).
|
|
2153
|
+
# When \`gh\` is unavailable, fall back to the legacy ff-merge path.
|
|
2154
|
+
if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
2155
|
+
if [ "\$_exit" -eq 0 ]; then
|
|
2156
|
+
if ( cd "\$WT" && _loop_is_doc_only_change ); then
|
|
2157
|
+
( cd "\$WT" && _loop_publish_doc_pr "\$BRANCH" "doc: loop cycle \${CYCLE_ID}" )
|
|
2158
|
+
else
|
|
2159
|
+
( cd "\$WT" && _loop_publish_pr "\$BRANCH" "loop cycle \${CYCLE_ID}" )
|
|
2160
|
+
fi
|
|
2161
|
+
_publish_status=\$?
|
|
2162
|
+
if [ "\$_publish_status" -eq 0 ]; then
|
|
2163
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2164
|
+
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
2165
|
+
elif [ "\$_publish_status" -eq 2 ]; then
|
|
2166
|
+
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
2167
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2168
|
+
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2169
|
+
else
|
|
2170
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh unavailable AND merge_back failed; worktree preserved at \$WT"
|
|
2171
|
+
echo "[loop] cycle \${CYCLE_ID}: gh+merge_back both failed; worktree preserved at \$WT"
|
|
2172
|
+
fi
|
|
2173
|
+
else
|
|
2174
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2175
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2176
|
+
fi
|
|
2177
|
+
else
|
|
2178
|
+
_worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2179
|
+
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2180
|
+
fi
|
|
2181
|
+
fi
|
|
2068
2182
|
INNER
|
|
2069
2183
|
chmod +x "$inner_path"
|
|
2070
2184
|
|
|
2071
|
-
local slug; slug=$(_project_slug "$project_path")
|
|
2072
2185
|
cat > "$script_path" << SCRIPT
|
|
2073
2186
|
#!/bin/bash -l
|
|
2074
2187
|
# Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
|
|
@@ -2218,8 +2331,13 @@ _install_launchd_plists() {
|
|
|
2218
2331
|
if [[ "$before" != "$after" ]]; then
|
|
2219
2332
|
updated=$((updated + 1))
|
|
2220
2333
|
if _launchd_is_loaded "$label"; then
|
|
2221
|
-
|
|
2222
|
-
|
|
2334
|
+
# FIX-027: use bootout/bootstrap so we don't disturb the label's
|
|
2335
|
+
# enabled flag in the launchd overrides db (which legacy
|
|
2336
|
+
# unload/load no-`-w` wipes on macOS Sonoma+, causing
|
|
2337
|
+
# `roll loop status` to falsely report off after `roll update`).
|
|
2338
|
+
local uid; uid=$(id -u)
|
|
2339
|
+
launchctl bootout "gui/${uid}/${label}" 2>/dev/null || true
|
|
2340
|
+
launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
|
|
2223
2341
|
fi
|
|
2224
2342
|
fi
|
|
2225
2343
|
done
|
|
@@ -2919,6 +3037,432 @@ EOF
|
|
|
2919
3037
|
return 0
|
|
2920
3038
|
}
|
|
2921
3039
|
|
|
3040
|
+
# FIX-032: dependency gate — parses BACKLOG inline tags so the loop SKILL
|
|
3041
|
+
# can enforce them at Step 2 (story pickup). Pure functions, no side effects.
|
|
3042
|
+
#
|
|
3043
|
+
# BACKLOG row format (relevant fragments):
|
|
3044
|
+
# | [US-AUTO-033](...) | desc `depends-on:US-AUTO-037` `manual-only:true` | 📋 Todo |
|
|
3045
|
+
# | FIX-100 | desc `depends-on:US-A,US-B` | 📋 Todo |
|
|
3046
|
+
#
|
|
3047
|
+
# Row matching is anchored on `^| [?<id>\b` so a story-id appearing in some
|
|
3048
|
+
# other row's depends-on list is not mistaken for the row that defines it.
|
|
3049
|
+
|
|
3050
|
+
# _loop_check_depends_on <story-id> [backlog-path]
|
|
3051
|
+
# Exit 0: all listed depends-on are ✅ Done, or no depends-on tag present.
|
|
3052
|
+
# Exit 1: any dep not ✅ Done, story-id not found, or backlog missing.
|
|
3053
|
+
# Stdout (on exit 1 due to unsatisfied deps): space-separated unsatisfied IDs.
|
|
3054
|
+
_loop_check_depends_on() {
|
|
3055
|
+
local id="$1"
|
|
3056
|
+
local backlog="${2:-BACKLOG.md}"
|
|
3057
|
+
[ -n "$id" ] || return 1
|
|
3058
|
+
[ -f "$backlog" ] || return 1
|
|
3059
|
+
|
|
3060
|
+
local row
|
|
3061
|
+
row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
|
|
3062
|
+
[ -n "$row" ] || return 1
|
|
3063
|
+
|
|
3064
|
+
local deps
|
|
3065
|
+
deps=$(echo "$row" | grep -oE 'depends-on:[A-Z][A-Z0-9,-]+' | head -1 | sed 's/depends-on://')
|
|
3066
|
+
[ -n "$deps" ] || return 0
|
|
3067
|
+
|
|
3068
|
+
local unsatisfied=""
|
|
3069
|
+
local dep
|
|
3070
|
+
local IFS_save="$IFS"
|
|
3071
|
+
IFS=','
|
|
3072
|
+
for dep in $deps; do
|
|
3073
|
+
local dep_row
|
|
3074
|
+
dep_row=$(grep -E "^\| \[?${dep}[]| ]" "$backlog" | head -1)
|
|
3075
|
+
if [ -z "$dep_row" ] || ! echo "$dep_row" | grep -qF '✅ Done'; then
|
|
3076
|
+
unsatisfied="${unsatisfied:+$unsatisfied }${dep}"
|
|
3077
|
+
fi
|
|
3078
|
+
done
|
|
3079
|
+
IFS="$IFS_save"
|
|
3080
|
+
|
|
3081
|
+
if [ -n "$unsatisfied" ]; then
|
|
3082
|
+
echo "$unsatisfied"
|
|
3083
|
+
return 1
|
|
3084
|
+
fi
|
|
3085
|
+
return 0
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
# _loop_is_manual_only <story-id> [backlog-path]
|
|
3089
|
+
# Exit 0: story's own row carries `manual-only:true`.
|
|
3090
|
+
# Exit 1: tag absent, story-id not found, or backlog missing.
|
|
3091
|
+
_loop_is_manual_only() {
|
|
3092
|
+
local id="$1"
|
|
3093
|
+
local backlog="${2:-BACKLOG.md}"
|
|
3094
|
+
[ -n "$id" ] || return 1
|
|
3095
|
+
[ -f "$backlog" ] || return 1
|
|
3096
|
+
|
|
3097
|
+
local row
|
|
3098
|
+
row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
|
|
3099
|
+
[ -n "$row" ] || return 1
|
|
3100
|
+
|
|
3101
|
+
echo "$row" | grep -qE 'manual-only:true'
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
# US-CL-004: changelog 风格守门 Phase 1 — mechanical linter.
|
|
3105
|
+
#
|
|
3106
|
+
# _changelog_lint_bullet <bullet-text>
|
|
3107
|
+
# Stdout: one violation tag per line; empty = bullet passes.
|
|
3108
|
+
# Exit: 0 always (callers read the output stream, not the exit code).
|
|
3109
|
+
#
|
|
3110
|
+
# Violation tags:
|
|
3111
|
+
# backtick-identifier `…` contains `_` or `()` (e.g. `_foo`, `bar()`)
|
|
3112
|
+
# file-suffix `.md`/`.sh`/`.yml`/`.ts`/`.bats` outside backticks
|
|
3113
|
+
# internal-word Phase N / Step N / Helper / Schema / Fixture / Refactor
|
|
3114
|
+
# over-length > 50 visible chars (UTF-8 codepoints; 中文按字符计)
|
|
3115
|
+
# path-fragment docs/ / bin/ / tests/ / scripts/ outside backticks
|
|
3116
|
+
#
|
|
3117
|
+
# Backticks are treated as the "user-quoted" zone — content there is assumed
|
|
3118
|
+
# to be a real user command (e.g. `roll edit notes.md`) and is excluded from
|
|
3119
|
+
# the file-suffix / path-fragment checks.
|
|
3120
|
+
_changelog_lint_bullet() {
|
|
3121
|
+
local bullet="$1"
|
|
3122
|
+
local stripped
|
|
3123
|
+
stripped=$(printf '%s' "$bullet" | sed -E 's/`[^`]*`//g')
|
|
3124
|
+
|
|
3125
|
+
if printf '%s' "$bullet" | grep -qE '`[^`]*(_|\(\))[^`]*`'; then
|
|
3126
|
+
echo "backtick-identifier"
|
|
3127
|
+
fi
|
|
3128
|
+
if printf '%s' "$stripped" | grep -qE '\.(md|sh|yml|ts|bats)([^A-Za-z0-9]|$)'; then
|
|
3129
|
+
echo "file-suffix"
|
|
3130
|
+
fi
|
|
3131
|
+
if printf '%s' "$bullet" | grep -qE '(Phase|Step)[[:space:]]+[0-9]+|Helper|Schema|Fixture|Refactor'; then
|
|
3132
|
+
echo "internal-word"
|
|
3133
|
+
fi
|
|
3134
|
+
local len
|
|
3135
|
+
len=$(printf '%s' "$bullet" | LC_ALL=C.UTF-8 wc -m | tr -d ' ')
|
|
3136
|
+
if [ "${len:-0}" -gt 50 ]; then
|
|
3137
|
+
echo "over-length"
|
|
3138
|
+
fi
|
|
3139
|
+
if printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
3140
|
+
echo "path-fragment"
|
|
3141
|
+
fi
|
|
3142
|
+
return 0
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
# US-CL-004: changelog few-shot style anchors — extract bullets from the
|
|
3146
|
+
# most recent 3 published `## v...` sections of CHANGELOG.md (skipping
|
|
3147
|
+
# `## Unreleased`). Cap at ~1500 chars so the agent's context stays lean.
|
|
3148
|
+
#
|
|
3149
|
+
# _changelog_style_anchors [changelog-path]
|
|
3150
|
+
# Stdout: concatenated bullet lines from the last 3 released versions.
|
|
3151
|
+
# Exit: 0 (empty output when no CHANGELOG.md or no released sections).
|
|
3152
|
+
_changelog_style_anchors() {
|
|
3153
|
+
local changelog="${1:-CHANGELOG.md}"
|
|
3154
|
+
[ -f "$changelog" ] || return 0
|
|
3155
|
+
awk '
|
|
3156
|
+
/^## v/ { ver++; if (ver > 3) exit; printing = 1; next }
|
|
3157
|
+
/^## / { printing = 0 }
|
|
3158
|
+
printing && /^- / { print }
|
|
3159
|
+
' "$changelog" | head -c 1500
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
# US-CL-005: changelog 风格守门 Phase 2 — self-audit gate.
|
|
3163
|
+
#
|
|
3164
|
+
# _changelog_audit_bullet <bullet>
|
|
3165
|
+
# Stricter than _changelog_lint_bullet: 5 boolean rules, 30-char cap.
|
|
3166
|
+
# Stdout: one failed-rule tag per line; empty = bullet passes.
|
|
3167
|
+
# Exit: 0 always.
|
|
3168
|
+
#
|
|
3169
|
+
# Rules:
|
|
3170
|
+
# over-length-30 visible chars > 30 AND no backtick (user-cmd escape hatch)
|
|
3171
|
+
# internal-id backtick content contains `_` or `()`
|
|
3172
|
+
# path-or-suffix .md/.sh/.yml/.ts/.bats or docs/bin/tests/scripts/ outside backticks
|
|
3173
|
+
# phase-step `Phase N` / `Step N` workflow vocabulary
|
|
3174
|
+
# bad-shape no `—` (em dash) AND no `不再` AND no `现在` keyword
|
|
3175
|
+
_changelog_audit_bullet() {
|
|
3176
|
+
local bullet="$1"
|
|
3177
|
+
local stripped
|
|
3178
|
+
stripped=$(printf '%s' "$bullet" | sed -E 's/`[^`]*`//g')
|
|
3179
|
+
|
|
3180
|
+
# Rule 1: length cap 30 (user-command in backticks bypasses this rule).
|
|
3181
|
+
if ! printf '%s' "$bullet" | grep -q '`'; then
|
|
3182
|
+
local len
|
|
3183
|
+
len=$(LC_ALL=en_US.UTF-8 printf '%s' "$bullet" | LC_ALL=en_US.UTF-8 wc -m | tr -d ' ')
|
|
3184
|
+
if [ "${len:-0}" -gt 30 ]; then
|
|
3185
|
+
echo "over-length-30"
|
|
3186
|
+
fi
|
|
3187
|
+
fi
|
|
3188
|
+
|
|
3189
|
+
# Rule 2: internal identifier inside backticks.
|
|
3190
|
+
if printf '%s' "$bullet" | grep -qE '`[^`]*(_|\(\))[^`]*`'; then
|
|
3191
|
+
echo "internal-id"
|
|
3192
|
+
fi
|
|
3193
|
+
|
|
3194
|
+
# Rule 3: file suffix / path fragment outside backticks.
|
|
3195
|
+
if printf '%s' "$stripped" | grep -qE '\.(md|sh|yml|ts|bats)([^A-Za-z0-9]|$)' \
|
|
3196
|
+
|| printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
3197
|
+
echo "path-or-suffix"
|
|
3198
|
+
fi
|
|
3199
|
+
|
|
3200
|
+
# Rule 4: workflow vocabulary.
|
|
3201
|
+
if printf '%s' "$bullet" | grep -qE '(Phase|Step)[[:space:]]+[0-9]+'; then
|
|
3202
|
+
echo "phase-step"
|
|
3203
|
+
fi
|
|
3204
|
+
|
|
3205
|
+
# Rule 5: required shape — em dash, 不再, or 现在.
|
|
3206
|
+
if ! printf '%s' "$bullet" | grep -qE '—|不再|现在'; then
|
|
3207
|
+
echo "bad-shape"
|
|
3208
|
+
fi
|
|
3209
|
+
|
|
3210
|
+
return 0
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
# _changelog_audit_log <verdict> <round> <bullet> [<reason>...]
|
|
3214
|
+
# Append a JSONL record to the audit log. Path overridable via
|
|
3215
|
+
# ROLL_CHANGELOG_AUDIT_LOG (tests use this to stay out of $HOME).
|
|
3216
|
+
_changelog_audit_log() {
|
|
3217
|
+
local verdict="$1" round="$2" bullet="$3"
|
|
3218
|
+
shift 3
|
|
3219
|
+
local log="${ROLL_CHANGELOG_AUDIT_LOG:-${_SHARED_ROOT}/loop/changelog-audit.jsonl}"
|
|
3220
|
+
mkdir -p "$(dirname "$log")"
|
|
3221
|
+
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
3222
|
+
local reasons_json='[]'
|
|
3223
|
+
if [ "$#" -gt 0 ]; then
|
|
3224
|
+
reasons_json=$(printf '%s\n' "$@" | jq -R . | jq -sc .)
|
|
3225
|
+
fi
|
|
3226
|
+
jq -nc \
|
|
3227
|
+
--arg ts "$ts" \
|
|
3228
|
+
--arg verdict "$verdict" \
|
|
3229
|
+
--argjson round "$round" \
|
|
3230
|
+
--arg bullet "$bullet" \
|
|
3231
|
+
--argjson reasons "$reasons_json" \
|
|
3232
|
+
'{ts:$ts, verdict:$verdict, round:$round, bullet:$bullet, reasons:$reasons}' \
|
|
3233
|
+
>> "$log"
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
# _changelog_audit_gate <round1> [<round2> <round3>]
|
|
3237
|
+
# Run up to 3 candidate bullets through _changelog_audit_bullet.
|
|
3238
|
+
# First clean candidate wins: print bullet to stdout, exit 0.
|
|
3239
|
+
# All 3 failed: print ⚠️-prefixed last candidate, append ALERT, exit 1.
|
|
3240
|
+
# Each round writes a _changelog_audit_log record.
|
|
3241
|
+
_changelog_audit_gate() {
|
|
3242
|
+
local i=0 last=""
|
|
3243
|
+
for candidate in "$@"; do
|
|
3244
|
+
i=$((i + 1))
|
|
3245
|
+
last="$candidate"
|
|
3246
|
+
local viols
|
|
3247
|
+
# shellcheck disable=SC2207
|
|
3248
|
+
viols=( $(_changelog_audit_bullet "$candidate") )
|
|
3249
|
+
if [ "${#viols[@]}" -eq 0 ]; then
|
|
3250
|
+
_changelog_audit_log pass "$i" "$candidate"
|
|
3251
|
+
printf '%s\n' "$candidate"
|
|
3252
|
+
return 0
|
|
3253
|
+
fi
|
|
3254
|
+
_changelog_audit_log fail "$i" "$candidate" "${viols[@]}"
|
|
3255
|
+
[ "$i" -ge 3 ] && break
|
|
3256
|
+
done
|
|
3257
|
+
# All 3 rounds failed (or fewer if caller passed < 3).
|
|
3258
|
+
mkdir -p "$(dirname "$_LOOP_ALERT")" 2>/dev/null
|
|
3259
|
+
{
|
|
3260
|
+
echo ""
|
|
3261
|
+
echo "# ALERT — changelog audit failed after $i rounds"
|
|
3262
|
+
echo "**Time**: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
3263
|
+
echo "**Bullet**: $last"
|
|
3264
|
+
echo "**Action**: kept under \`## Unreleased\` with ⚠️ prefix; human review recommended."
|
|
3265
|
+
} >> "$_LOOP_ALERT"
|
|
3266
|
+
printf '⚠️ %s\n' "$last"
|
|
3267
|
+
return 1
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
# US-AUTO-036: worktree helpers (loop-safe pure additions).
|
|
3271
|
+
#
|
|
3272
|
+
# Phase 1 of worktree isolation — these helpers are NOT yet called by
|
|
3273
|
+
# runner.sh. US-AUTO-037 (manual-only) wires them into
|
|
3274
|
+
# _write_loop_runner_script. Do not delete or inline; they are unit-tested
|
|
3275
|
+
# in tests/unit/roll_worktree.bats.
|
|
3276
|
+
|
|
3277
|
+
# _worktree_path <slug> <us-id>
|
|
3278
|
+
# Echoes the canonical worktree directory for a (project, story) pair.
|
|
3279
|
+
_worktree_path() {
|
|
3280
|
+
echo "${_SHARED_ROOT}/worktrees/${1}-${2}"
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
# _worktree_alert <msg>
|
|
3284
|
+
# Append a timestamped line to $_LOOP_ALERT. Used by failure paths in
|
|
3285
|
+
# _worktree_merge_back to surface stuck worktrees.
|
|
3286
|
+
_worktree_alert() {
|
|
3287
|
+
mkdir -p "$(dirname "$_LOOP_ALERT")" 2>/dev/null
|
|
3288
|
+
printf '[%s] worktree: %s\n' "$(date -u +%FT%TZ)" "$1" >> "$_LOOP_ALERT"
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
# _worktree_create <path> <branch> <base>
|
|
3292
|
+
# Create a worktree at <path> on a new branch <branch> rooted at <base>.
|
|
3293
|
+
# Idempotent: if <branch> already exists locally (from a prior failed
|
|
3294
|
+
# run) it is deleted first so `git worktree add -b` does not error.
|
|
3295
|
+
_worktree_create() {
|
|
3296
|
+
local path="$1" branch="$2" base="$3"
|
|
3297
|
+
mkdir -p "$(dirname "$path")"
|
|
3298
|
+
if [ -e "$path" ]; then
|
|
3299
|
+
git worktree remove --force "$path" 2>/dev/null || true
|
|
3300
|
+
rm -rf "$path" 2>/dev/null || true
|
|
3301
|
+
fi
|
|
3302
|
+
if git show-ref --verify --quiet "refs/heads/${branch}"; then
|
|
3303
|
+
git branch -D "$branch" >/dev/null 2>&1 || true
|
|
3304
|
+
fi
|
|
3305
|
+
git worktree add "$path" -b "$branch" "$base"
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
# _worktree_cleanup <path> <branch>
|
|
3309
|
+
# Remove the worktree at <path> and delete <branch>. Tolerant when
|
|
3310
|
+
# either is already absent so retries / partial-failure rollback is safe.
|
|
3311
|
+
_worktree_cleanup() {
|
|
3312
|
+
local path="$1" branch="$2"
|
|
3313
|
+
git worktree remove --force "$path" 2>/dev/null || true
|
|
3314
|
+
rm -rf "$path" 2>/dev/null || true
|
|
3315
|
+
git branch -D "$branch" 2>/dev/null || true
|
|
3316
|
+
return 0
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
# _worktree_fetch_origin <branch>
|
|
3320
|
+
# `git fetch origin <branch>` quietly. Lenient on failure: a missing
|
|
3321
|
+
# remote / network blip must not derail the loop, so we return 0 even
|
|
3322
|
+
# when fetch fails (the loop's later ff-only check is the strict gate).
|
|
3323
|
+
_worktree_fetch_origin() {
|
|
3324
|
+
local branch="$1"
|
|
3325
|
+
if ! git fetch origin "$branch" --quiet 2>/dev/null; then
|
|
3326
|
+
echo "[worktree] fetch origin ${branch} failed (lenient, continuing)" >&2
|
|
3327
|
+
fi
|
|
3328
|
+
return 0
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
# _worktree_submodule_init <path>
|
|
3332
|
+
# Run `git submodule update --init --recursive` inside the worktree at
|
|
3333
|
+
# <path> so its working tree is materially complete. Runs in a subshell
|
|
3334
|
+
# (cd is local) so the caller's cwd and the parent worktree's submodule
|
|
3335
|
+
# state are untouched. Returns submodule update's exit code.
|
|
3336
|
+
_worktree_submodule_init() {
|
|
3337
|
+
local path="$1"
|
|
3338
|
+
( cd "$path" && git submodule update --init --recursive --quiet )
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
# _worktree_merge_back <branch>
|
|
3342
|
+
# Caller must be in the main worktree (cwd = main). Steps:
|
|
3343
|
+
# 1. git pull --ff-only origin main (sync local main with remote)
|
|
3344
|
+
# 2. git merge --ff-only <branch> (linear merge of loop branch)
|
|
3345
|
+
# 3. git push origin main (publish)
|
|
3346
|
+
# Any failure → write to $_LOOP_ALERT and return 1 (worktree is left
|
|
3347
|
+
# in place by the caller for human inspection, per US-AUTO-036 non-goal).
|
|
3348
|
+
_worktree_merge_back() {
|
|
3349
|
+
local branch="$1"
|
|
3350
|
+
if ! git pull --ff-only origin main --quiet 2>/dev/null; then
|
|
3351
|
+
_worktree_alert "pull --ff-only origin main failed (remote diverged?)"
|
|
3352
|
+
return 1
|
|
3353
|
+
fi
|
|
3354
|
+
if ! git merge --ff-only "$branch" --quiet 2>/dev/null; then
|
|
3355
|
+
_worktree_alert "merge --ff-only ${branch} failed (not fast-forwardable from main)"
|
|
3356
|
+
return 1
|
|
3357
|
+
fi
|
|
3358
|
+
if ! git push origin main --quiet 2>/dev/null; then
|
|
3359
|
+
_worktree_alert "push origin main failed after merging ${branch}"
|
|
3360
|
+
return 1
|
|
3361
|
+
fi
|
|
3362
|
+
return 0
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
3366
|
+
#
|
|
3367
|
+
# _loop_publish_pr <branch> [title]
|
|
3368
|
+
# Caller's cwd: a tree where <branch> exists locally.
|
|
3369
|
+
# Steps:
|
|
3370
|
+
# 1. git push origin <branch>
|
|
3371
|
+
# 2. gh pr view <branch> → reuse if a PR is already open
|
|
3372
|
+
# 3. gh pr create --base main --head <branch> ...
|
|
3373
|
+
# 4. gh pr merge <branch> --auto --squash --delete-branch
|
|
3374
|
+
# Stdout: PR URL (always, even on idempotent reuse).
|
|
3375
|
+
# Exit 0 on success / idempotent reuse; non-zero on push or create failure.
|
|
3376
|
+
# On auto-merge failure: still returns 0 (PR exists; human can take over).
|
|
3377
|
+
# When `gh` is not installed: returns 2 — runner script's fallback path.
|
|
3378
|
+
_loop_publish_pr() {
|
|
3379
|
+
local branch="$1"
|
|
3380
|
+
local title="${2:-loop cycle ${branch#loop/}}"
|
|
3381
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
3382
|
+
_worktree_alert "_loop_publish_pr: gh not installed; cannot publish PR for ${branch}"
|
|
3383
|
+
return 2
|
|
3384
|
+
fi
|
|
3385
|
+
local slug; slug=$(_gh_repo_slug 2>/dev/null) || slug=""
|
|
3386
|
+
if [ -z "$slug" ]; then
|
|
3387
|
+
_worktree_alert "_loop_publish_pr: origin remote is not a github repo; cannot publish PR for ${branch}"
|
|
3388
|
+
return 2
|
|
3389
|
+
fi
|
|
3390
|
+
local _push_err
|
|
3391
|
+
_push_err=$(git push origin "$branch" 2>&1) || {
|
|
3392
|
+
_worktree_alert "_loop_publish_pr: push origin ${branch} failed: ${_push_err}"
|
|
3393
|
+
return 1
|
|
3394
|
+
}
|
|
3395
|
+
local pr_url
|
|
3396
|
+
pr_url=$(gh -R "$slug" pr view "$branch" --json url -q .url 2>/dev/null) || pr_url=""
|
|
3397
|
+
if [ -z "$pr_url" ]; then
|
|
3398
|
+
local body
|
|
3399
|
+
body=$(printf 'Auto-opened by roll-loop cycle.\n\n- Branch: %s\n- TCR micro-commits: %s\n\nThis PR will auto-merge once required checks pass.' \
|
|
3400
|
+
"$branch" "$(git rev-list --count origin/main.."$branch" 2>/dev/null || echo '?')")
|
|
3401
|
+
pr_url=$(gh -R "$slug" pr create --base main --head "$branch" \
|
|
3402
|
+
--title "$title" --body "$body" 2>/dev/null) || pr_url=""
|
|
3403
|
+
if [ -z "$pr_url" ]; then
|
|
3404
|
+
_worktree_alert "_loop_publish_pr: gh pr create failed for ${branch}"
|
|
3405
|
+
return 1
|
|
3406
|
+
fi
|
|
3407
|
+
fi
|
|
3408
|
+
gh -R "$slug" pr merge "$branch" --auto --squash --delete-branch >/dev/null 2>&1 \
|
|
3409
|
+
|| _worktree_alert "_loop_publish_pr: gh pr merge --auto failed for ${branch} (PR ${pr_url} left open)"
|
|
3410
|
+
echo "$pr_url"
|
|
3411
|
+
return 0
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
# _loop_is_doc_only_change
|
|
3415
|
+
# Returns 0 if every file changed since origin/main is doc-only
|
|
3416
|
+
# (BACKLOG.md, CHANGELOG.md, PROPOSALS.md, docs/, .claude/).
|
|
3417
|
+
# Returns 1 if any code file changed or there are no changes.
|
|
3418
|
+
_loop_is_doc_only_change() {
|
|
3419
|
+
local changed
|
|
3420
|
+
changed=$(git diff --name-only origin/main HEAD 2>/dev/null) || return 1
|
|
3421
|
+
[ -z "$changed" ] && return 1
|
|
3422
|
+
echo "$changed" | grep -qvE '^(BACKLOG\.md|CHANGELOG\.md|PROPOSALS\.md|docs/|\.claude/)' && return 1
|
|
3423
|
+
return 0
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
# _loop_publish_doc_pr <branch> [title]
|
|
3427
|
+
# Like _loop_publish_pr but merges immediately with --admin (no CI wait).
|
|
3428
|
+
# For doc-only changes where CI is not meaningful.
|
|
3429
|
+
_loop_publish_doc_pr() {
|
|
3430
|
+
local branch="$1"
|
|
3431
|
+
local title="${2:-doc update ${branch#loop/}}"
|
|
3432
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
3433
|
+
_worktree_alert "_loop_publish_doc_pr: gh not installed; cannot publish PR for ${branch}"
|
|
3434
|
+
return 2
|
|
3435
|
+
fi
|
|
3436
|
+
local slug; slug=$(_gh_repo_slug 2>/dev/null) || slug=""
|
|
3437
|
+
if [ -z "$slug" ]; then
|
|
3438
|
+
_worktree_alert "_loop_publish_doc_pr: origin remote is not a github repo; cannot publish PR for ${branch}"
|
|
3439
|
+
return 2
|
|
3440
|
+
fi
|
|
3441
|
+
if ! git push origin "$branch" --quiet 2>/dev/null; then
|
|
3442
|
+
_worktree_alert "_loop_publish_doc_pr: push origin ${branch} failed"
|
|
3443
|
+
return 1
|
|
3444
|
+
fi
|
|
3445
|
+
local pr_url
|
|
3446
|
+
pr_url=$(gh -R "$slug" pr view "$branch" --json url -q .url 2>/dev/null) || pr_url=""
|
|
3447
|
+
if [ -z "$pr_url" ]; then
|
|
3448
|
+
local body
|
|
3449
|
+
body=$(printf 'Doc-only update by roll-loop cycle.\n\n- Branch: %s\n- Files: BACKLOG / docs only\n\nMerging immediately — no CI gate needed for doc-only changes.' "$branch")
|
|
3450
|
+
pr_url=$(gh -R "$slug" pr create --base main --head "$branch" \
|
|
3451
|
+
--title "$title" --body "$body" 2>/dev/null) || pr_url=""
|
|
3452
|
+
if [ -z "$pr_url" ]; then
|
|
3453
|
+
_worktree_alert "_loop_publish_doc_pr: gh pr create failed for ${branch}"
|
|
3454
|
+
return 1
|
|
3455
|
+
fi
|
|
3456
|
+
fi
|
|
3457
|
+
if ! gh -R "$slug" pr merge "$branch" --admin --squash --delete-branch >/dev/null 2>&1; then
|
|
3458
|
+
_worktree_alert "_loop_publish_doc_pr: gh pr merge --admin failed for ${branch} (PR ${pr_url} left open)"
|
|
3459
|
+
echo "$pr_url"
|
|
3460
|
+
return 1
|
|
3461
|
+
fi
|
|
3462
|
+
echo "$pr_url"
|
|
3463
|
+
return 0
|
|
3464
|
+
}
|
|
3465
|
+
|
|
2922
3466
|
_loop_monitor() {
|
|
2923
3467
|
local interval="${1:-3}"
|
|
2924
3468
|
local project_path; project_path=$(pwd -P)
|
|
@@ -3416,83 +3960,378 @@ cmd_backlog() {
|
|
|
3416
3960
|
fi
|
|
3417
3961
|
}
|
|
3418
3962
|
|
|
3963
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3964
|
+
# DASHBOARD — 自治优先六块布局 (US-AUTO-029)
|
|
3419
3965
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
3420
3966
|
|
|
3967
|
+
# ① Identity — git working tree state.
|
|
3968
|
+
_dash_git_status() {
|
|
3969
|
+
git rev-parse --is-inside-work-tree &>/dev/null || { echo "—"; return; }
|
|
3970
|
+
if [[ -z "$(git status --porcelain 2>/dev/null)" ]]; then
|
|
3971
|
+
echo "✓"
|
|
3972
|
+
else
|
|
3973
|
+
echo "dirty"
|
|
3974
|
+
fi
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
# ② Loop layer: extract in-progress story id|title|feature-link from BACKLOG.md.
|
|
3978
|
+
# Output empty if no row's *status column* is 🔨 In Progress (substring matches
|
|
3979
|
+
# anywhere on the row would catch description text that mentions the emoji).
|
|
3980
|
+
_dash_in_progress_story() {
|
|
3981
|
+
[[ -f "BACKLOG.md" ]] || return 0
|
|
3982
|
+
local row
|
|
3983
|
+
row=$(grep -F '| 🔨 In Progress |' BACKLOG.md | head -1) || return 0
|
|
3984
|
+
[[ -z "$row" ]] && return 0
|
|
3985
|
+
local id desc
|
|
3986
|
+
id=$(echo "$row" | grep -oE '(US|FIX|REFACTOR)-[A-Z]*-?[0-9]+' | head -1)
|
|
3987
|
+
desc=$(echo "$row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-60)
|
|
3988
|
+
local link
|
|
3989
|
+
link=$(echo "$row" | grep -oE 'docs/features/[^)]+' | head -1 || true)
|
|
3990
|
+
printf '%s|%s|%s' "$id" "$desc" "$link"
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
# ② Loop layer: minutes since last "tcr:" commit, or empty if none.
|
|
3994
|
+
_dash_last_tcr_minutes() {
|
|
3995
|
+
git rev-parse --is-inside-work-tree &>/dev/null || return 0
|
|
3996
|
+
local last_ts
|
|
3997
|
+
last_ts=$(git log --grep='^tcr:' -1 --format=%ct 2>/dev/null)
|
|
3998
|
+
[[ -z "$last_ts" ]] && return 0
|
|
3999
|
+
local now; now=$(date +%s)
|
|
4000
|
+
echo $(( (now - last_ts) / 60 ))
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
# ② Loop layer: tcr: commits since midnight today.
|
|
4004
|
+
_dash_tcr_today_count() {
|
|
4005
|
+
git rev-parse --is-inside-work-tree &>/dev/null || { echo 0; return; }
|
|
4006
|
+
local since; since=$(date '+%Y-%m-%d 00:00:00')
|
|
4007
|
+
git log --since="$since" --grep='^tcr:' --oneline 2>/dev/null | grep -c '^' || echo 0
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
# ② Dream layer: hours since last dream log entry on disk.
|
|
4011
|
+
_dash_last_dream_hours() {
|
|
4012
|
+
local dream_log="${HOME}/.shared/roll/dream/log.md"
|
|
4013
|
+
[[ -f "$dream_log" ]] || return 0
|
|
4014
|
+
local mod_time now
|
|
4015
|
+
mod_time=$(stat -c %Y "$dream_log" 2>/dev/null || stat -f %m "$dream_log" 2>/dev/null || echo 0)
|
|
4016
|
+
now=$(date +%s)
|
|
4017
|
+
echo $(( (now - mod_time) / 3600 ))
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
# ② Dream layer: count of REFACTOR-XXX rows currently 📋 Todo in BACKLOG.
|
|
4021
|
+
_dash_refactor_pending() {
|
|
4022
|
+
[[ -f "BACKLOG.md" ]] || { echo 0; return; }
|
|
4023
|
+
grep -E '^\| REFACTOR-' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' '
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
# ② Peer layer: last result + days ago from peer log, empty if no log.
|
|
4027
|
+
_dash_last_peer() {
|
|
4028
|
+
local peer_log_dir="${HOME}/.shared/roll/peer"
|
|
4029
|
+
local latest
|
|
4030
|
+
latest=$(ls "$peer_log_dir"/*.log 2>/dev/null | sort | tail -1 || true)
|
|
4031
|
+
[[ -z "$latest" || ! -f "$latest" ]] && return 0
|
|
4032
|
+
local result
|
|
4033
|
+
result=$(grep -oE '(AGREE|REFINE|OBJECT|ESCALATE)' "$latest" 2>/dev/null | tail -1 || true)
|
|
4034
|
+
local mod_time now days
|
|
4035
|
+
mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
|
|
4036
|
+
now=$(date +%s)
|
|
4037
|
+
days=$(( (now - mod_time) / 86400 ))
|
|
4038
|
+
printf '%s|%s' "${result:-—}" "${days}"
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
# ③ Pipeline counts → Idea Backlog Build (Verify/Release reserved).
|
|
4042
|
+
_dash_pipeline_counts() {
|
|
4043
|
+
[[ -f "BACKLOG.md" ]] || { echo "0 0 0 0 0"; return; }
|
|
4044
|
+
local idea backlog build
|
|
4045
|
+
idea=$(grep -E '^\| IDEA-' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
|
|
4046
|
+
backlog=$(grep -E '^\| (\[?US-|FIX-|REFACTOR-)' BACKLOG.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
|
|
4047
|
+
build=$(grep -F '| 🔨 In Progress |' BACKLOG.md 2>/dev/null | wc -l | tr -d ' ')
|
|
4048
|
+
printf '%s %s %s 0 0' "$idea" "$backlog" "$build"
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
# ④ DoD AC signal — read [x]/total checkboxes for a US section in feature doc.
|
|
4052
|
+
# Echoes "x/total"; "0/0" if no checkboxes found.
|
|
4053
|
+
_dash_ac_completion() {
|
|
4054
|
+
local feature_link="$1"
|
|
4055
|
+
[[ -z "$feature_link" ]] && { echo "0/0"; return; }
|
|
4056
|
+
local path="${feature_link%%#*}"
|
|
4057
|
+
local anchor="${feature_link##*#}"
|
|
4058
|
+
[[ ! -f "$path" ]] && { echo "0/0"; return; }
|
|
4059
|
+
# Extract the section from <a id="anchor"></a> or ## heading to next ## heading.
|
|
4060
|
+
local section
|
|
4061
|
+
section=$(awk -v anc="$anchor" '
|
|
4062
|
+
BEGIN{in_sec=0}
|
|
4063
|
+
/^<a id="/{
|
|
4064
|
+
gsub(/<a id="|"><\/a>/, "")
|
|
4065
|
+
if ($0 == anc) { in_sec=1; next }
|
|
4066
|
+
}
|
|
4067
|
+
in_sec && /^## /{ if(!started){ started=1; next } else { exit } }
|
|
4068
|
+
in_sec && started { print }
|
|
4069
|
+
in_sec { started_default=1 }
|
|
4070
|
+
' "$path" 2>/dev/null)
|
|
4071
|
+
[[ -z "$section" ]] && {
|
|
4072
|
+
# Fallback: match heading line containing the anchor pattern directly.
|
|
4073
|
+
section=$(awk -v pat="$anchor" 'BEGIN{IGNORECASE=1}
|
|
4074
|
+
tolower($0) ~ pat && /^## /{p=1;next}
|
|
4075
|
+
p && /^## /{exit}
|
|
4076
|
+
p{print}' "$path" 2>/dev/null)
|
|
4077
|
+
}
|
|
4078
|
+
local done total
|
|
4079
|
+
done=$(echo "$section" | grep -cE '\[x\]' || echo 0)
|
|
4080
|
+
total=$(echo "$section" | grep -cE '\[[ x]\]' || echo 0)
|
|
4081
|
+
printf '%s/%s' "$done" "$total"
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
# ④ DoD CI signal — query gh for HEAD's most-recent run conclusion.
|
|
4085
|
+
# Returns: success | pending | failure | none
|
|
4086
|
+
_dash_ci_status() {
|
|
4087
|
+
command -v gh &>/dev/null || { echo "none"; return; }
|
|
4088
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { echo "none"; return; }
|
|
4089
|
+
local slug; slug=$(_gh_repo_slug 2>/dev/null) || true
|
|
4090
|
+
local out
|
|
4091
|
+
if [[ -n "$slug" ]]; then
|
|
4092
|
+
out=$(gh -R "$slug" run list --commit "$commit" --json status,conclusion 2>/dev/null) || { echo "none"; return; }
|
|
4093
|
+
else
|
|
4094
|
+
out=$(gh run list --commit "$commit" --json status,conclusion 2>/dev/null) || { echo "none"; return; }
|
|
4095
|
+
fi
|
|
4096
|
+
[[ -z "$out" || "$out" == "[]" ]] && { echo "none"; return; }
|
|
4097
|
+
local concl status
|
|
4098
|
+
concl=$(echo "$out" | jq -r '.[0].conclusion // ""' 2>/dev/null)
|
|
4099
|
+
status=$(echo "$out" | jq -r '.[0].status // ""' 2>/dev/null)
|
|
4100
|
+
if [[ "$status" == "in_progress" || "$status" == "queued" ]]; then
|
|
4101
|
+
echo "pending"
|
|
4102
|
+
elif [[ "$concl" == "success" ]]; then
|
|
4103
|
+
echo "success"
|
|
4104
|
+
elif [[ -n "$concl" ]]; then
|
|
4105
|
+
echo "failure"
|
|
4106
|
+
else
|
|
4107
|
+
echo "pending"
|
|
4108
|
+
fi
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
# ⑤ Active ALERT count (number of "# ALERT" headings in ALERT.md, 0 if absent).
|
|
4112
|
+
_dash_alert_count() {
|
|
4113
|
+
[[ -f "$_LOOP_ALERT" ]] || { echo 0; return; }
|
|
4114
|
+
grep '^# ALERT' "$_LOOP_ALERT" 2>/dev/null | wc -l | tr -d ' '
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
# ⑤ Pending proposal count — "## PROPOSAL:" entries in PROPOSALS.md.
|
|
4118
|
+
_dash_proposal_count() {
|
|
4119
|
+
[[ -f "PROPOSALS.md" ]] || { echo 0; return; }
|
|
4120
|
+
grep '^## PROPOSAL' PROPOSALS.md 2>/dev/null | wc -l | tr -d ' '
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
# ⑤ Release-ready signal — true iff there are releasable commits since the
|
|
4124
|
+
# latest tag AND the latest brief signals 可发版/Release ready. Releasable =
|
|
4125
|
+
# any commit since the latest tag whose subject does NOT start with the
|
|
4126
|
+
# release-irrelevant prefixes `docs:` or `chore:`. Prevents the flag from
|
|
4127
|
+
# sticking on after a release when only docs rewrites land on top of the tag
|
|
4128
|
+
# (FIX-033 symptom 2).
|
|
4129
|
+
_dash_release_ready() {
|
|
4130
|
+
local latest_tag
|
|
4131
|
+
latest_tag=$(git describe --tags --abbrev=0 2>/dev/null) || return 1
|
|
4132
|
+
local commits_with_code
|
|
4133
|
+
commits_with_code=$(git log "${latest_tag}..HEAD" --pretty=format:%s 2>/dev/null \
|
|
4134
|
+
| grep -cvE '^(docs|chore)(\([^)]*\))?:[[:space:]]' 2>/dev/null \
|
|
4135
|
+
|| echo 0)
|
|
4136
|
+
[[ "${commits_with_code:-0}" -gt 0 ]] || return 1
|
|
4137
|
+
local latest
|
|
4138
|
+
latest=$(ls docs/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
4139
|
+
[[ -z "$latest" ]] && return 1
|
|
4140
|
+
grep -qE '✅ 可发版|Release ready' "$latest" 2>/dev/null
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
# ⑥ Latest brief summary — first non-trivial line after frontmatter.
|
|
4144
|
+
_dash_brief_summary() {
|
|
4145
|
+
local latest="$1"
|
|
4146
|
+
[[ -z "$latest" || ! -f "$latest" ]] && return 0
|
|
4147
|
+
awk '
|
|
4148
|
+
NR==1 && /^#/ { next } # skip H1 title
|
|
4149
|
+
/^>/ { next } # skip blockquote
|
|
4150
|
+
/^---$/ { next }
|
|
4151
|
+
/^$/ { next }
|
|
4152
|
+
/^## /{ gsub(/^## */,""); print; exit }
|
|
4153
|
+
/^[^[:space:]]/{ print; exit }
|
|
4154
|
+
' "$latest" 2>/dev/null | head -1 | cut -c1-60
|
|
4155
|
+
}
|
|
4156
|
+
|
|
3421
4157
|
_dashboard() {
|
|
3422
4158
|
local project_path; project_path=$(pwd -P)
|
|
3423
4159
|
local project_name; project_name=$(basename "$project_path")
|
|
3424
4160
|
local agent; agent=$(_project_agent)
|
|
4161
|
+
local git_state; git_state=$(_dash_git_status)
|
|
4162
|
+
local is_darwin=false
|
|
4163
|
+
[[ "$(uname)" == "Darwin" ]] && is_darwin=true
|
|
3425
4164
|
|
|
3426
|
-
|
|
4165
|
+
# ── ① Identity ─────────────────────────────────────────────────────────────
|
|
4166
|
+
echo ""
|
|
4167
|
+
printf " ${BOLD}${CYAN}%s${NC} ${YELLOW}v%s${NC} ${BOLD}·${NC} agent ${CYAN}%s${NC} ${BOLD}·${NC} git " \
|
|
4168
|
+
"$project_name" "$VERSION" "$agent"
|
|
4169
|
+
case "$git_state" in
|
|
4170
|
+
✓) printf "${GREEN}✓${NC}\n" ;;
|
|
4171
|
+
dirty) printf "${YELLOW}dirty${NC}\n" ;;
|
|
4172
|
+
*) printf "${YELLOW}%s${NC}\n" "$git_state" ;;
|
|
4173
|
+
esac
|
|
4174
|
+
echo ""
|
|
4175
|
+
|
|
4176
|
+
# ── ② AI 自治 — 三层 × 四道防线 (主视觉) ────────────────────────────────
|
|
4177
|
+
echo -e " ${BOLD}╔══ 🤖 AI 自治 — 三层 × 四道防线 ══════════════════════════╗${NC}"
|
|
3427
4178
|
|
|
4179
|
+
# Loop layer
|
|
4180
|
+
local loop_state="not-installed"
|
|
3428
4181
|
local _dash_loop_paused=false
|
|
3429
4182
|
[[ -f "$_LOOP_STATE" ]] && grep -q "^status: paused" "$_LOOP_STATE" 2>/dev/null && _dash_loop_paused=true
|
|
4183
|
+
if $is_darwin; then
|
|
4184
|
+
loop_state=$(_launchd_svc_state "loop" "$project_path")
|
|
4185
|
+
else
|
|
4186
|
+
crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_state="enabled"
|
|
4187
|
+
fi
|
|
4188
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
4189
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
4190
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
4191
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
4192
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
4193
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
4194
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
4195
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
3430
4196
|
|
|
3431
|
-
local
|
|
3432
|
-
|
|
3433
|
-
|
|
4197
|
+
local loop_badge loop_sched
|
|
4198
|
+
loop_sched=$(printf "every :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
|
|
4199
|
+
case "$loop_state" in
|
|
4200
|
+
enabled) loop_badge="${GREEN}● enabled${NC}" ;;
|
|
4201
|
+
installed-off) loop_badge="${YELLOW}⚠ off${NC}" ;;
|
|
4202
|
+
*) loop_badge="${RED}○ missing${NC}" ;;
|
|
4203
|
+
esac
|
|
4204
|
+
$_dash_loop_paused && loop_badge="${YELLOW}⏸ paused${NC}"
|
|
4205
|
+
printf " Loop Layer %b %s\n" "$loop_badge" "$loop_sched"
|
|
4206
|
+
|
|
4207
|
+
# Loop "Now:" line — current in-progress story, if any.
|
|
4208
|
+
local in_prog; in_prog=$(_dash_in_progress_story)
|
|
4209
|
+
if [[ -n "$in_prog" ]]; then
|
|
4210
|
+
local p_id p_desc
|
|
4211
|
+
p_id=${in_prog%%|*}
|
|
4212
|
+
p_desc=$(echo "$in_prog" | awk -F'|' '{print $2}')
|
|
4213
|
+
printf " Now: ${BOLD}🔨 %s${NC} %s\n" "$p_id" "$p_desc"
|
|
3434
4214
|
else
|
|
3435
|
-
|
|
4215
|
+
printf " Now: ${DIM:-}idle${NC}\n"
|
|
3436
4216
|
fi
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
esac
|
|
3464
|
-
done
|
|
3465
|
-
fi
|
|
4217
|
+
|
|
4218
|
+
# last TCR + today count
|
|
4219
|
+
local last_tcr_min today_tcr
|
|
4220
|
+
last_tcr_min=$(_dash_last_tcr_minutes)
|
|
4221
|
+
today_tcr=$(_dash_tcr_today_count)
|
|
4222
|
+
if [[ -n "$last_tcr_min" ]]; then
|
|
4223
|
+
printf " last TCR ${CYAN}%smin${NC} ago · ${CYAN}%s${NC} micro-commits today\n" "$last_tcr_min" "$today_tcr"
|
|
4224
|
+
else
|
|
4225
|
+
printf " no tcr commits yet\n"
|
|
4226
|
+
fi
|
|
4227
|
+
|
|
4228
|
+
# Dream layer
|
|
4229
|
+
local dream_state="not-installed"
|
|
4230
|
+
$is_darwin && dream_state=$(_launchd_svc_state "dream" "$project_path")
|
|
4231
|
+
local dream_badge
|
|
4232
|
+
case "$dream_state" in
|
|
4233
|
+
enabled) dream_badge="${GREEN}● enabled${NC}" ;;
|
|
4234
|
+
installed-off) dream_badge="${YELLOW}⚠ off${NC}" ;;
|
|
4235
|
+
*) dream_badge="${RED}○ missing${NC}" ;;
|
|
4236
|
+
esac
|
|
4237
|
+
printf " Dream Layer %b %02d:%02d\n" "$dream_badge" "$dream_hour" "$dream_minute"
|
|
4238
|
+
local dream_hours refac_pending
|
|
4239
|
+
dream_hours=$(_dash_last_dream_hours)
|
|
4240
|
+
refac_pending=$(_dash_refactor_pending)
|
|
4241
|
+
if [[ -n "$dream_hours" ]]; then
|
|
4242
|
+
printf " Last scan ${CYAN}%sh${NC} ago → ${CYAN}%s${NC} REFACTOR queued\n" "$dream_hours" "$refac_pending"
|
|
3466
4243
|
else
|
|
3467
|
-
|
|
4244
|
+
printf " no scan yet → ${CYAN}%s${NC} REFACTOR queued\n" "$refac_pending"
|
|
4245
|
+
fi
|
|
4246
|
+
|
|
4247
|
+
# Peer layer
|
|
4248
|
+
local peer; peer=$(_dash_last_peer)
|
|
4249
|
+
printf " Peer Layer ${GREEN}● ready${NC} on complexity=large\n"
|
|
4250
|
+
if [[ -n "$peer" ]]; then
|
|
4251
|
+
local peer_res peer_days
|
|
4252
|
+
peer_res=${peer%%|*}
|
|
4253
|
+
peer_days=${peer##*|}
|
|
4254
|
+
printf " Last call ${CYAN}%sd${NC} ago · %s\n" "$peer_days" "$peer_res"
|
|
4255
|
+
else
|
|
4256
|
+
printf " Last call —\n"
|
|
4257
|
+
fi
|
|
4258
|
+
|
|
4259
|
+
# 四道防线
|
|
4260
|
+
echo -e " ${BOLD}─ 四道防线 ─${NC}"
|
|
4261
|
+
local def_tcr="${RED}○${NC}" def_review="${GREEN}●${NC}" def_spar="${YELLOW}○${NC}" def_sentinel="${YELLOW}○ off${NC}"
|
|
4262
|
+
if [[ -n "$last_tcr_min" ]]; then
|
|
4263
|
+
def_tcr="${GREEN}● ${last_tcr_min}min${NC}"
|
|
3468
4264
|
fi
|
|
3469
|
-
|
|
4265
|
+
printf " TCR %b Spar %b Auto Review %b Sentinel %b\n" \
|
|
4266
|
+
"$def_tcr" "$def_spar" "$def_review" "$def_sentinel"
|
|
4267
|
+
echo -e " ${BOLD}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
4268
|
+
echo ""
|
|
4269
|
+
|
|
4270
|
+
# ── ③ Pipeline 全景 ────────────────────────────────────────────────────────
|
|
4271
|
+
read -r pl_idea pl_backlog pl_build pl_verify pl_release <<< "$(_dash_pipeline_counts)"
|
|
4272
|
+
local build_color="${DIM:-}"
|
|
4273
|
+
(( pl_build > 0 )) && build_color="${BOLD}${YELLOW}"
|
|
4274
|
+
printf " ${BOLD}📦 Pipeline${NC} Idea %s ▸ Backlog %s ▸ Build %b%s🔨${NC} ▸ Verify %s ▸ Release %s\n" \
|
|
4275
|
+
"$pl_idea" "$pl_backlog" "$build_color" "$pl_build" "$pl_verify" "$pl_release"
|
|
4276
|
+
echo ""
|
|
3470
4277
|
|
|
3471
|
-
#
|
|
3472
|
-
if [[ -
|
|
3473
|
-
local
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
4278
|
+
# ── ④ Current Focus · DoD (仅当 Build > 0) ──────────────────────────────
|
|
4279
|
+
if [[ -n "$in_prog" && "$pl_build" -gt 0 ]]; then
|
|
4280
|
+
local p_id p_desc p_link
|
|
4281
|
+
p_id=${in_prog%%|*}
|
|
4282
|
+
p_desc=$(echo "$in_prog" | awk -F'|' '{print $2}')
|
|
4283
|
+
p_link=$(echo "$in_prog" | awk -F'|' '{print $3}')
|
|
4284
|
+
local ac_ratio; ac_ratio=$(_dash_ac_completion "$p_link")
|
|
4285
|
+
local ac_done="${ac_ratio%%/*}" ac_total="${ac_ratio##*/}"
|
|
4286
|
+
local ac_badge ci_badge
|
|
4287
|
+
if [[ "$ac_total" != "0" && "$ac_done" == "$ac_total" ]]; then
|
|
4288
|
+
ac_badge="${GREEN}✓ AC${NC}"
|
|
3477
4289
|
else
|
|
3478
|
-
|
|
4290
|
+
ac_badge="${YELLOW}○ AC ${ac_done}/${ac_total}${NC}"
|
|
3479
4291
|
fi
|
|
4292
|
+
local ci_state; ci_state=$(_dash_ci_status)
|
|
4293
|
+
case "$ci_state" in
|
|
4294
|
+
success) ci_badge="${GREEN}✓ CI${NC}" ;;
|
|
4295
|
+
pending) ci_badge="${YELLOW}… CI${NC}" ;;
|
|
4296
|
+
failure) ci_badge="${RED}✗ CI${NC}" ;;
|
|
4297
|
+
*) ci_badge="${YELLOW}○ CI${NC}" ;;
|
|
4298
|
+
esac
|
|
4299
|
+
printf " ${BOLD}📊 Current Focus · DoD${NC}\n"
|
|
4300
|
+
printf " 🔨 ${BOLD}%s${NC} %s\n" "$p_id" "$p_desc"
|
|
4301
|
+
printf " [%b] [%b]\n" "$ac_badge" "$ci_badge"
|
|
4302
|
+
printf " ${YELLOW}其余 4 项 DoD 信号源待接入:see US-AUTO-030/031, IDEA-013/014${NC}\n"
|
|
4303
|
+
echo ""
|
|
3480
4304
|
fi
|
|
3481
4305
|
|
|
3482
|
-
|
|
3483
|
-
local
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
echo -e "\n Brief (${age}h ago) ${readiness:+${CYAN}${readiness}${NC}}"
|
|
3491
|
-
[[ "$done_count" -gt 0 ]] && echo -e " This cycle: ${done_count} items completed"
|
|
4306
|
+
# ── ⑤ Human × AI 介入区 ───────────────────────────────────────────────────
|
|
4307
|
+
local alerts proposals release_ready
|
|
4308
|
+
alerts=$(_dash_alert_count); alerts=${alerts//[^0-9]/}; alerts=${alerts:-0}
|
|
4309
|
+
proposals=$(_dash_proposal_count); proposals=${proposals//[^0-9]/}; proposals=${proposals:-0}
|
|
4310
|
+
release_ready=false; _dash_release_ready && release_ready=true
|
|
4311
|
+
printf " ${BOLD}👤 需要你介入${NC}\n"
|
|
4312
|
+
if (( alerts == 0 )) && (( proposals == 0 )) && ! $release_ready; then
|
|
4313
|
+
printf " ${GREEN}✓ AI 自驱中 — 无需介入${NC}\n"
|
|
3492
4314
|
else
|
|
3493
|
-
|
|
4315
|
+
(( alerts > 0 )) && printf " ${RED}⚠ %s ALERT${NC} run: roll alert\n" "$alerts"
|
|
4316
|
+
(( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} see: PROPOSALS.md\n" "$proposals"
|
|
4317
|
+
$release_ready && printf " ${GREEN}✓ Release ready${NC} run: roll release\n"
|
|
3494
4318
|
fi
|
|
4319
|
+
echo ""
|
|
3495
4320
|
|
|
4321
|
+
# ── ⑥ Schedules & Last Brief ──────────────────────────────────────────────
|
|
4322
|
+
printf " ${BOLD}⏰ Schedules & Last Brief${NC}\n"
|
|
4323
|
+
printf " loop :%02d · dream %02d:%02d · brief %02d:%02d\n" \
|
|
4324
|
+
"$loop_minute" "$dream_hour" "$dream_minute" "$brief_hour" "$brief_minute"
|
|
4325
|
+
local latest_brief; latest_brief=$(ls docs/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
4326
|
+
if [[ -n "$latest_brief" ]]; then
|
|
4327
|
+
local mod_time now age summary
|
|
4328
|
+
mod_time=$(stat -c %Y "$latest_brief" 2>/dev/null || stat -f %m "$latest_brief" 2>/dev/null || echo 0)
|
|
4329
|
+
now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
|
|
4330
|
+
summary=$(_dash_brief_summary "$latest_brief")
|
|
4331
|
+
printf " Brief ${CYAN}%sh${NC} ago — %s\n" "$age" "${summary:-—}"
|
|
4332
|
+
else
|
|
4333
|
+
printf " Brief: ${YELLOW}none yet${NC} — run: roll brief\n"
|
|
4334
|
+
fi
|
|
3496
4335
|
echo ""
|
|
3497
4336
|
}
|
|
3498
4337
|
|