@seanyao/roll 2026.512.8 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.513.1
4
+ - **Added**: loop worktree 隔离 Phase 2 — `_write_loop_runner_script` 现在让每轮 cron 在独立的 `loop/cycle-<ts>-<pid>` worktree 里跑 claude,结束后 ff-merge 回 main + 自动清理;失败保留 worktree + 写 ALERT。loop 不再吞 main 的 WIP,多轮之间也完全隔离。claude 仍保留 story selection 权(SKILL.md 不变)
5
+ - **Added**: loop worktree 隔离 Phase 1 — `bin/roll` 新增 7 个 `_worktree_*` helpers(path / create 幂等 / cleanup / fetch lenient / submodule init / merge_back ff-only / alert),覆盖 loop 在独立 worktree 跑 story 的完整生命周期;零行 runner.sh 改动,loop 自己也能跑;US-AUTO-037 (manual-only) 之后把这些 helpers 接入 `_write_loop_runner_script`
6
+ - **Added**: loop 依赖闸门 — BACKLOG 行末尾的 `` `depends-on:US-X,US-Y` `` 和 `` `manual-only:true` `` 标签从此具有强制力;loop 选 story 前先用 `_loop_check_depends_on` / `_loop_is_manual_only` 两个 helper 过一遍,未满足依赖或带 manual-only 标的 story 直接跳过并写 `skipped` runs.jsonl 记录,不再需要靠人盯标签
7
+ - **Fixed**: loop 并发保护补丁 — 2026-05-13 14:37 实测同 runner 下出现并发 claude 会话同时改 state.yaml / BACKLOG / git;inner script 现新增二级 LOCK(PID + start-ts 4h 双校验),守住 claude 调用现场,外层 LOCK 即使被旁路也兜底
8
+ - **Changed**: CI 拆 unit / integration 双 job 并行 — `tests/run.sh` 接受可选目录参数;`.github/workflows/ci.yml` 用 `strategy.matrix` 把 509 unit + 70 integration 用例分到两个 runner 并行跑(REFACTOR-009 Phase 1B);Phase 2 测试瘦身拆到 REFACTOR-010
9
+ - **Changed**: `roll`(无参)dashboard 重设计 — 自治优先六块布局:① Identity(项目名 + 版本 + agent + git 状态)② AI 自治主视觉(Loop / Dream / Peer 三层 + 四道防线,框线高亮)③ Pipeline 全景(Idea/Backlog/Build/Verify/Release 五段计数)④ Current Focus · DoD(in-progress story 的 [AC] [CI] 信号,其余 4 项标注待接入)⑤ Human × AI 介入区(ALERT/PROPOSAL/Release ready,空时显示"AI 自驱中")⑥ Schedules & Last Brief。把"AI 自动跑什么"放第一眼,不再埋在 `roll loop status` 子命令里
10
+ - **Changed**: CI 测试运行器 (`tests/run.sh`) 动态检测核数(`nproc`/`sysctl`)替换硬编码 `--jobs 4`,可用 `ROLL_TEST_JOBS` 覆盖;bats-core submodule 缺失时给出清晰报错
11
+ - **Changed**: GitHub Actions CI 跳过 docs-only PR(`paths-ignore: **.md, docs/**`),文档改动不再触发全套 bats
12
+ - **Fixed**: `roll peer` 在 REFINE/OBJECT 退出路径上的 "resolution: unbound variable" 风险 — cmd_peer 显式初始化 `local resolution=""`,并补齐 AGREE/REFINE/OBJECT/ESCALATE/UNKNOWN 五条退出路径的回归测试
13
+ - **Fixed**: `roll update` 后 `roll loop status` 误报 off — `_install_launchd_plists` reload 路径改用 `launchctl bootout` + `bootstrap`(不动 overrides db),消除 macOS Sonoma+ 上 no-`-w` unload/load 把 label `enabled` 标记吞掉的副作用;不再需要手动 `roll loop on` 恢复
14
+
3
15
  ## v2026.512.8
4
16
  - **Added**: `$roll-doc` — legacy 项目文档自动化技能:四阶段扫描(索引 + 缺口分析 + 草稿补全 + 报告),支持 `--dry-run` / `--force`,适用任何项目
5
17
  - **Added**: `roll-.dream` Scan 6 — 文档新鲜度检测(滞后文档 / 未记录 ENV 变量 / 架构文档缺失),依赖 roll-doc,发现写入 REFACTOR 条目
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean YAO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/roll CHANGED
@@ -4,7 +4,7 @@ set -euo pipefail
4
4
  # Roll — AI Agent Convention Manager
5
5
  # Single source of truth for how all AI coding agents behave.
6
6
 
7
- VERSION="2026.512.8"
7
+ VERSION="2026.513.1"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -1437,7 +1437,7 @@ _peer_auto_attach() {
1437
1437
  fi
1438
1438
  local launched=0
1439
1439
  if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
1440
- open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
1440
+ open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
1441
1441
  fi
1442
1442
  if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
1443
1443
  osascript \
@@ -1447,7 +1447,7 @@ _peer_auto_attach() {
1447
1447
  && launched=1 || true
1448
1448
  fi
1449
1449
  if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
1450
- open -na Ghostty.app --args -e "tmux attach -t $session" >/dev/null 2>&1 && launched=1 || true
1450
+ open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
1451
1451
  fi
1452
1452
  if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
1453
1453
  osascript \
@@ -1666,6 +1666,8 @@ cmd_peer() {
1666
1666
  peer_session="roll-peer-${from_tool}-${to_tool}"
1667
1667
  if ! tmux has-session -t "$peer_session" 2>/dev/null; then
1668
1668
  tmux new-session -d -s "$peer_session" -x 200 -y 50
1669
+ fi
1670
+ if [ -z "$(tmux list-clients -t "$peer_session" 2>/dev/null)" ]; then
1669
1671
  _peer_auto_attach "$peer_session"
1670
1672
  fi
1671
1673
  fi
@@ -1707,7 +1709,7 @@ cmd_peer() {
1707
1709
 
1708
1710
  printf '%s\n' "$response" >> "$log_file"
1709
1711
 
1710
- local resolution
1712
+ local resolution=""
1711
1713
  resolution="$(_peer_parse_resolution "$response")"
1712
1714
 
1713
1715
  if [[ -z "$resolution" ]]; then
@@ -1732,7 +1734,7 @@ cmd_peer() {
1732
1734
  if [[ "$round" -ge 3 ]]; then
1733
1735
  warn "Max rounds reached. Escalating to user. 达到最大轮数,升级给用户。"
1734
1736
  else
1735
- info "Peer requests $resolution. Continue to round $((round + 1)). Peer 请求 $resolution,继续第 $((round + 1)) 轮。"
1737
+ info "Peer requests ${resolution}. Continue to round $((round + 1)). Peer 请求 ${resolution},继续第 $((round + 1)) 轮。"
1736
1738
  fi
1737
1739
  ;;
1738
1740
  ESCALATE|UNKNOWN)
@@ -2046,17 +2048,67 @@ _write_loop_runner_script() {
2046
2048
  # Use stream-json + formatter: --verbose alone does nothing in -p mode;
2047
2049
  # stream-json enables realtime streaming; loop-fmt.py humanizes the events.
2048
2050
  local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
2051
+ local roll_bin="${ROLL_PKG_DIR}/bin/roll"
2049
2052
  local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
2053
+ # US-AUTO-037: strip leading `cd "<path>" && ` (callers like
2054
+ # _install_launchd_plists prepend it). The runner now manages cwd itself
2055
+ # — pointing at the worktree when isolation succeeds, project_path otherwise.
2056
+ local claude_cmd; claude_cmd="${cmd_verbose#cd \"*\" && }"
2057
+ local slug; slug=$(_project_slug "$project_path")
2050
2058
  cat > "$inner_path" << INNER
2051
2059
  #!/bin/bash -l
2052
2060
  set -o pipefail
2053
2061
  export PATH="/opt/homebrew/bin:\$PATH"
2062
+ # FIX-031: inner-level LOCK (PID + start-ts) — outer runner.sh LOCK can be
2063
+ # bypassed (recovery / retry / direct invocation); this guards the actual
2064
+ # claude invocation so a second session can't run under the same project.
2065
+ INNER_LOCK="\$(dirname "\$0")/.INNER-LOCK-\$(basename "\$0" -inner.sh | sed 's/^run-//')"
2066
+ if [ -f "\$INNER_LOCK" ]; then
2067
+ _prev_pid=""; _prev_ts=""
2068
+ IFS=: read -r _prev_pid _prev_ts < "\$INNER_LOCK" 2>/dev/null || true
2069
+ _now=\$(date -u +%s)
2070
+ if [ -n "\$_prev_pid" ] && [ -n "\$_prev_ts" ] \\
2071
+ && kill -0 "\$_prev_pid" 2>/dev/null \\
2072
+ && [ "\$((_now - _prev_ts))" -lt 14400 ]; then
2073
+ echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] inner loop already running (PID \$_prev_pid), skipping"
2074
+ exit 0
2075
+ fi
2076
+ rm -f "\$INNER_LOCK"
2077
+ fi
2078
+ printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
2079
+ trap 'rm -f "\$INNER_LOCK"' EXIT
2080
+
2081
+ # US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
2082
+ # safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
2083
+ # bin/roll's top-level \`set -euo pipefail\` infects us, so disable -e (the
2084
+ # retry loop relies on tolerating non-zero exits) while keeping pipefail.
2085
+ source "${roll_bin}"
2086
+ set +e
2087
+
2088
+ # Pre-claude: try to create a per-cycle isolated worktree on origin/main.
2089
+ # On any failure (no remote, no main, etc.) fall back to running in the
2090
+ # project's main tree (degraded — no isolation, like pre-037 behavior).
2091
+ CYCLE_ID="\$(date -u +%Y%m%d-%H%M%S)-\$\$"
2092
+ WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
2093
+ BRANCH="loop/cycle-\${CYCLE_ID}"
2094
+ _USE_WORKTREE=0
2095
+ cd "${project_path}" 2>/dev/null || true
2096
+ if _worktree_fetch_origin main \\
2097
+ && _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
2098
+ _USE_WORKTREE=1
2099
+ _worktree_submodule_init "\$WT" 2>/dev/null || true
2100
+ echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
2101
+ else
2102
+ echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; running in main tree (no isolation)"
2103
+ WT="${project_path}"
2104
+ fi
2105
+
2054
2106
  FMT="${fmt_script}"
2055
2107
  for _attempt in 1 2 3; do
2056
2108
  if [ -f "\$FMT" ]; then
2057
- ( cd "${project_path}" && ${cmd_verbose} ) | python3 "\$FMT"
2109
+ ( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
2058
2110
  else
2059
- ( cd "${project_path}" && ${cmd_verbose} )
2111
+ ( cd "\$WT" && ${claude_cmd} )
2060
2112
  fi
2061
2113
  _exit=\$?
2062
2114
  [ "\$_exit" -eq 0 ] && break
@@ -2065,10 +2117,25 @@ for _attempt in 1 2 3; do
2065
2117
  sleep 30
2066
2118
  fi
2067
2119
  done
2120
+
2121
+ # Post-claude: merge back if we used an isolated worktree.
2122
+ if [ "\$_USE_WORKTREE" = "1" ]; then
2123
+ if [ "\$_exit" -eq 0 ]; then
2124
+ if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
2125
+ _worktree_cleanup "\$WT" "\$BRANCH"
2126
+ echo "[loop] cycle \${CYCLE_ID}: merged and cleaned up"
2127
+ else
2128
+ _worktree_alert "cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT (branch \$BRANCH)"
2129
+ echo "[loop] cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT"
2130
+ fi
2131
+ else
2132
+ _worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
2133
+ echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
2134
+ fi
2135
+ fi
2068
2136
  INNER
2069
2137
  chmod +x "$inner_path"
2070
2138
 
2071
- local slug; slug=$(_project_slug "$project_path")
2072
2139
  cat > "$script_path" << SCRIPT
2073
2140
  #!/bin/bash -l
2074
2141
  # Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
@@ -2218,8 +2285,13 @@ _install_launchd_plists() {
2218
2285
  if [[ "$before" != "$after" ]]; then
2219
2286
  updated=$((updated + 1))
2220
2287
  if _launchd_is_loaded "$label"; then
2221
- launchctl unload "$plist" 2>/dev/null || true
2222
- launchctl load "$plist" 2>/dev/null || true
2288
+ # FIX-027: use bootout/bootstrap so we don't disturb the label's
2289
+ # enabled flag in the launchd overrides db (which legacy
2290
+ # unload/load no-`-w` wipes on macOS Sonoma+, causing
2291
+ # `roll loop status` to falsely report off after `roll update`).
2292
+ local uid; uid=$(id -u)
2293
+ launchctl bootout "gui/${uid}/${label}" 2>/dev/null || true
2294
+ launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
2223
2295
  fi
2224
2296
  fi
2225
2297
  done
@@ -2919,6 +2991,165 @@ EOF
2919
2991
  return 0
2920
2992
  }
2921
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
+
2922
3153
  _loop_monitor() {
2923
3154
  local interval="${1:-3}"
2924
3155
  local project_path; project_path=$(pwd -P)
@@ -3416,83 +3647,366 @@ cmd_backlog() {
3416
3647
  fi
3417
3648
  }
3418
3649
 
3650
+ # ─────────────────────────────────────────────────────────────────────────────
3651
+ # DASHBOARD — 自治优先六块布局 (US-AUTO-029)
3419
3652
  # ─────────────────────────────────────────────────────────────────────────────
3420
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
+ }
3831
+
3421
3832
  _dashboard() {
3422
3833
  local project_path; project_path=$(pwd -P)
3423
3834
  local project_name; project_name=$(basename "$project_path")
3424
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
3425
3839
 
3426
- echo -e "\n ${BOLD}${CYAN}${project_name}${NC} ${YELLOW}v${VERSION}${NC}\n"
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 ""
3427
3850
 
3851
+ # ── ② AI 自治 — 三层 × 四道防线 (主视觉) ────────────────────────────────
3852
+ echo -e " ${BOLD}╔══ 🤖 AI 自治 — 三层 × 四道防线 ══════════════════════════╗${NC}"
3853
+
3854
+ # Loop layer
3855
+ local loop_state="not-installed"
3428
3856
  local _dash_loop_paused=false
3429
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)")
3430
3871
 
3431
- local _dash_loop_enabled=false
3432
- if [[ "$(uname)" == "Darwin" ]]; then
3433
- _launchd_is_loaded "$(_launchd_label "loop" "$project_path")" && _dash_loop_enabled=true
3872
+ local loop_badge loop_sched
3873
+ loop_sched=$(printf "every :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
3874
+ case "$loop_state" in
3875
+ enabled) loop_badge="${GREEN}● enabled${NC}" ;;
3876
+ installed-off) loop_badge="${YELLOW}⚠ off${NC}" ;;
3877
+ *) loop_badge="${RED}○ missing${NC}" ;;
3878
+ esac
3879
+ $_dash_loop_paused && loop_badge="${YELLOW}⏸ paused${NC}"
3880
+ printf " Loop Layer %b %s\n" "$loop_badge" "$loop_sched"
3881
+
3882
+ # Loop "Now:" line — current in-progress story, if any.
3883
+ local in_prog; in_prog=$(_dash_in_progress_story)
3884
+ if [[ -n "$in_prog" ]]; then
3885
+ local p_id p_desc
3886
+ p_id=${in_prog%%|*}
3887
+ p_desc=$(echo "$in_prog" | awk -F'|' '{print $2}')
3888
+ printf " Now: ${BOLD}🔨 %s${NC} %s\n" "$p_id" "$p_desc"
3434
3889
  else
3435
- crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && _dash_loop_enabled=true
3890
+ printf " Now: ${DIM:-}idle${NC}\n"
3436
3891
  fi
3437
- if $_dash_loop_paused; then
3438
- echo -e " Loop ${YELLOW}⏸ paused${NC} run: roll loop resume"
3439
- elif $_dash_loop_enabled; then
3440
- echo -e " Loop ${GREEN}● on${NC} Agent: ${CYAN}${agent}${NC}"
3441
- if [[ "$(uname)" == "Darwin" ]]; then
3442
- local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
3443
- active_start=$(_config_read_int "loop_active_start" "10")
3444
- active_end=$(_config_read_int "loop_active_end" "18")
3445
- loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
3446
- dream_hour=$(_config_read_int "loop_dream_hour" "3")
3447
- dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
3448
- brief_hour=$(_config_read_int "loop_brief_hour" "9")
3449
- brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
3450
- local loop_sched dream_sched brief_sched
3451
- loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
3452
- dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
3453
- brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
3454
- local svcs=("loop" "dream" "brief")
3455
- local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
3456
- for i in "${!svcs[@]}"; do
3457
- local svc="${svcs[$i]}" schedule="${scheds[$i]}"
3458
- local state; state=$(_launchd_svc_state "$svc" "$project_path")
3459
- case "$state" in
3460
- enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
3461
- installed-off) printf " ${YELLOW}%-8s ⚠ off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
3462
- not-installed) printf " ${RED}%-8s missing${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
3463
- esac
3464
- done
3465
- 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"
3899
+ else
3900
+ printf " no tcr commits yet\n"
3901
+ fi
3902
+
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"
3466
3918
  else
3467
- echo -e " Loop ${YELLOW}○ off${NC} run: roll loop on"
3919
+ printf " no scan yet → ${CYAN}%s${NC} REFACTOR queued\n" "$refac_pending"
3468
3920
  fi
3469
- [[ -f "$_LOOP_ALERT" ]] && echo -e " ${RED}⚠ ALERT — run: roll alert${NC}"
3470
3921
 
3471
- # Backlog summary
3472
- if [[ -f "BACKLOG.md" ]]; then
3473
- local pending_count
3474
- pending_count=$(grep -cF '| 📋 Todo |' "BACKLOG.md" 2>/dev/null) || pending_count=0
3475
- if (( pending_count > 0 )); then
3476
- echo -e " Backlog ${YELLOW}${pending_count} pending${NC} run: roll backlog"
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}"
3477
3964
  else
3478
- echo -e " Backlog ${GREEN} clear${NC}"
3965
+ ac_badge="${YELLOW} AC ${ac_done}/${ac_total}${NC}"
3479
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 ""
3480
3979
  fi
3481
3980
 
3482
- local briefs_dir="docs/briefs"
3483
- local latest; latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
3484
- if [[ -n "$latest" ]]; then
3485
- local mod_time now age
3486
- mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
3487
- now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
3488
- local readiness; readiness=$(awk '/^## Release Readiness/{found=1;next} found && /[^[:space:]]/{gsub(/\*\*/,""); print; exit}' "$latest" 2>/dev/null || true)
3489
- local done_count; done_count=$(grep -c '| Story\|| Fix\|| Refactor' "$latest" 2>/dev/null) || done_count=0
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"
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"
3492
3989
  else
3493
- echo -e "\n No brief yet run: roll brief"
3990
+ (( alerts > 0 )) && printf " ${RED}⚠ %s ALERT${NC} run: roll alert\n" "$alerts"
3991
+ (( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} run: roll backlog\n" "$proposals"
3992
+ $release_ready && printf " ${GREEN}✓ Release ready${NC} run: roll release\n"
3494
3993
  fi
3994
+ echo ""
3495
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
3496
4010
  echo ""
3497
4011
  }
3498
4012
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.512.8",
3
+ "version": "2026.513.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -95,6 +95,25 @@ Priority: FIX-XXX first (bugs block progress), then US-XXX, then REFACTOR-XXX.
95
95
  - An earlier loop iteration that hasn't finished yet (rare; should be guarded by LOCK)
96
96
  - A previous interrupted run (the resume logic in Step 1 will pick these up)
97
97
 
98
+ **Dependency gate** (FIX-032). For each `📋 Todo` candidate, before picking:
99
+
100
+ ```bash
101
+ # Source bin/roll once per cycle, then call the helpers per candidate.
102
+ source "$(command -v roll)"
103
+
104
+ bash -c 'source "$(command -v roll)"; _loop_is_manual_only "<story-id>" BACKLOG.md'
105
+ # exit 0 → row has `manual-only:true` → SKIP this story, log to runs.jsonl
106
+ # `skipped`, append INFO line ("manual-only — requires $roll-build")
107
+
108
+ bash -c 'source "$(command -v roll)"; _loop_check_depends_on "<story-id>" BACKLOG.md'
109
+ # exit 0 → all `depends-on:US-X,US-Y` are ✅ Done → eligible
110
+ # exit 1 → stdout lists unsatisfied dep IDs; SKIP this story, log to
111
+ # runs.jsonl `skipped` with reason "depends-on: <unsatisfied>"
112
+ ```
113
+
114
+ Move to the next candidate when skipping. The two gates are pure functions
115
+ over BACKLOG.md text — no side effects, no LOCK interaction.
116
+
98
117
  Cap at `max_items_per_run` to limit blast radius per cycle.
99
118
 
100
119
  ### Concurrency Safety