@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 +12 -0
- package/LICENSE +21 -0
- package/bin/roll +578 -64
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +19 -0
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.
|
|
7
|
+
VERSION="2026.513.1"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -1437,7 +1437,7 @@ _peer_auto_attach() {
|
|
|
1437
1437
|
fi
|
|
1438
1438
|
local launched=0
|
|
1439
1439
|
if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
|
|
1440
|
-
open -na Ghostty.app --args -e
|
|
1440
|
+
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1441
1441
|
fi
|
|
1442
1442
|
if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
|
|
1443
1443
|
osascript \
|
|
@@ -1447,7 +1447,7 @@ _peer_auto_attach() {
|
|
|
1447
1447
|
&& launched=1 || true
|
|
1448
1448
|
fi
|
|
1449
1449
|
if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
|
|
1450
|
-
open -na Ghostty.app --args -e
|
|
1450
|
+
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1451
1451
|
fi
|
|
1452
1452
|
if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
|
|
1453
1453
|
osascript \
|
|
@@ -1666,6 +1666,8 @@ cmd_peer() {
|
|
|
1666
1666
|
peer_session="roll-peer-${from_tool}-${to_tool}"
|
|
1667
1667
|
if ! tmux has-session -t "$peer_session" 2>/dev/null; then
|
|
1668
1668
|
tmux new-session -d -s "$peer_session" -x 200 -y 50
|
|
1669
|
+
fi
|
|
1670
|
+
if [ -z "$(tmux list-clients -t "$peer_session" 2>/dev/null)" ]; then
|
|
1669
1671
|
_peer_auto_attach "$peer_session"
|
|
1670
1672
|
fi
|
|
1671
1673
|
fi
|
|
@@ -1707,7 +1709,7 @@ cmd_peer() {
|
|
|
1707
1709
|
|
|
1708
1710
|
printf '%s\n' "$response" >> "$log_file"
|
|
1709
1711
|
|
|
1710
|
-
local resolution
|
|
1712
|
+
local resolution=""
|
|
1711
1713
|
resolution="$(_peer_parse_resolution "$response")"
|
|
1712
1714
|
|
|
1713
1715
|
if [[ -z "$resolution" ]]; then
|
|
@@ -1732,7 +1734,7 @@ cmd_peer() {
|
|
|
1732
1734
|
if [[ "$round" -ge 3 ]]; then
|
|
1733
1735
|
warn "Max rounds reached. Escalating to user. 达到最大轮数,升级给用户。"
|
|
1734
1736
|
else
|
|
1735
|
-
info "Peer requests $resolution. Continue to round $((round + 1)). Peer 请求 $resolution,继续第 $((round + 1)) 轮。"
|
|
1737
|
+
info "Peer requests ${resolution}. Continue to round $((round + 1)). Peer 请求 ${resolution},继续第 $((round + 1)) 轮。"
|
|
1736
1738
|
fi
|
|
1737
1739
|
;;
|
|
1738
1740
|
ESCALATE|UNKNOWN)
|
|
@@ -2046,17 +2048,67 @@ _write_loop_runner_script() {
|
|
|
2046
2048
|
# Use stream-json + formatter: --verbose alone does nothing in -p mode;
|
|
2047
2049
|
# stream-json enables realtime streaming; loop-fmt.py humanizes the events.
|
|
2048
2050
|
local fmt_script="${ROLL_PKG_DIR}/lib/loop-fmt.py"
|
|
2051
|
+
local roll_bin="${ROLL_PKG_DIR}/bin/roll"
|
|
2049
2052
|
local cmd_verbose="${cmd/claude -p/claude -p --verbose --output-format stream-json}"
|
|
2053
|
+
# US-AUTO-037: strip leading `cd "<path>" && ` (callers like
|
|
2054
|
+
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
2055
|
+
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
2056
|
+
local claude_cmd; claude_cmd="${cmd_verbose#cd \"*\" && }"
|
|
2057
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
2050
2058
|
cat > "$inner_path" << INNER
|
|
2051
2059
|
#!/bin/bash -l
|
|
2052
2060
|
set -o pipefail
|
|
2053
2061
|
export PATH="/opt/homebrew/bin:\$PATH"
|
|
2062
|
+
# FIX-031: inner-level LOCK (PID + start-ts) — outer runner.sh LOCK can be
|
|
2063
|
+
# bypassed (recovery / retry / direct invocation); this guards the actual
|
|
2064
|
+
# claude invocation so a second session can't run under the same project.
|
|
2065
|
+
INNER_LOCK="\$(dirname "\$0")/.INNER-LOCK-\$(basename "\$0" -inner.sh | sed 's/^run-//')"
|
|
2066
|
+
if [ -f "\$INNER_LOCK" ]; then
|
|
2067
|
+
_prev_pid=""; _prev_ts=""
|
|
2068
|
+
IFS=: read -r _prev_pid _prev_ts < "\$INNER_LOCK" 2>/dev/null || true
|
|
2069
|
+
_now=\$(date -u +%s)
|
|
2070
|
+
if [ -n "\$_prev_pid" ] && [ -n "\$_prev_ts" ] \\
|
|
2071
|
+
&& kill -0 "\$_prev_pid" 2>/dev/null \\
|
|
2072
|
+
&& [ "\$((_now - _prev_ts))" -lt 14400 ]; then
|
|
2073
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] inner loop already running (PID \$_prev_pid), skipping"
|
|
2074
|
+
exit 0
|
|
2075
|
+
fi
|
|
2076
|
+
rm -f "\$INNER_LOCK"
|
|
2077
|
+
fi
|
|
2078
|
+
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
2079
|
+
trap 'rm -f "\$INNER_LOCK"' EXIT
|
|
2080
|
+
|
|
2081
|
+
# US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
|
|
2082
|
+
# safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
|
|
2083
|
+
# bin/roll's top-level \`set -euo pipefail\` infects us, so disable -e (the
|
|
2084
|
+
# retry loop relies on tolerating non-zero exits) while keeping pipefail.
|
|
2085
|
+
source "${roll_bin}"
|
|
2086
|
+
set +e
|
|
2087
|
+
|
|
2088
|
+
# Pre-claude: try to create a per-cycle isolated worktree on origin/main.
|
|
2089
|
+
# On any failure (no remote, no main, etc.) fall back to running in the
|
|
2090
|
+
# project's main tree (degraded — no isolation, like pre-037 behavior).
|
|
2091
|
+
CYCLE_ID="\$(date -u +%Y%m%d-%H%M%S)-\$\$"
|
|
2092
|
+
WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
2093
|
+
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2094
|
+
_USE_WORKTREE=0
|
|
2095
|
+
cd "${project_path}" 2>/dev/null || true
|
|
2096
|
+
if _worktree_fetch_origin main \\
|
|
2097
|
+
&& _worktree_create "\$WT" "\$BRANCH" "origin/main"; then
|
|
2098
|
+
_USE_WORKTREE=1
|
|
2099
|
+
_worktree_submodule_init "\$WT" 2>/dev/null || true
|
|
2100
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
2101
|
+
else
|
|
2102
|
+
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; running in main tree (no isolation)"
|
|
2103
|
+
WT="${project_path}"
|
|
2104
|
+
fi
|
|
2105
|
+
|
|
2054
2106
|
FMT="${fmt_script}"
|
|
2055
2107
|
for _attempt in 1 2 3; do
|
|
2056
2108
|
if [ -f "\$FMT" ]; then
|
|
2057
|
-
( cd "
|
|
2109
|
+
( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
|
|
2058
2110
|
else
|
|
2059
|
-
( cd "
|
|
2111
|
+
( cd "\$WT" && ${claude_cmd} )
|
|
2060
2112
|
fi
|
|
2061
2113
|
_exit=\$?
|
|
2062
2114
|
[ "\$_exit" -eq 0 ] && break
|
|
@@ -2065,10 +2117,25 @@ for _attempt in 1 2 3; do
|
|
|
2065
2117
|
sleep 30
|
|
2066
2118
|
fi
|
|
2067
2119
|
done
|
|
2120
|
+
|
|
2121
|
+
# Post-claude: merge back if we used an isolated worktree.
|
|
2122
|
+
if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
2123
|
+
if [ "\$_exit" -eq 0 ]; then
|
|
2124
|
+
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
2125
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2126
|
+
echo "[loop] cycle \${CYCLE_ID}: merged and cleaned up"
|
|
2127
|
+
else
|
|
2128
|
+
_worktree_alert "cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2129
|
+
echo "[loop] cycle \${CYCLE_ID}: merge_back failed; worktree preserved at \$WT"
|
|
2130
|
+
fi
|
|
2131
|
+
else
|
|
2132
|
+
_worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2133
|
+
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2134
|
+
fi
|
|
2135
|
+
fi
|
|
2068
2136
|
INNER
|
|
2069
2137
|
chmod +x "$inner_path"
|
|
2070
2138
|
|
|
2071
|
-
local slug; slug=$(_project_slug "$project_path")
|
|
2072
2139
|
cat > "$script_path" << SCRIPT
|
|
2073
2140
|
#!/bin/bash -l
|
|
2074
2141
|
# Active-window check — skipped when ROLL_LOOP_FORCE is set (manual 'roll loop now')
|
|
@@ -2218,8 +2285,13 @@ _install_launchd_plists() {
|
|
|
2218
2285
|
if [[ "$before" != "$after" ]]; then
|
|
2219
2286
|
updated=$((updated + 1))
|
|
2220
2287
|
if _launchd_is_loaded "$label"; then
|
|
2221
|
-
|
|
2222
|
-
|
|
2288
|
+
# FIX-027: use bootout/bootstrap so we don't disturb the label's
|
|
2289
|
+
# enabled flag in the launchd overrides db (which legacy
|
|
2290
|
+
# unload/load no-`-w` wipes on macOS Sonoma+, causing
|
|
2291
|
+
# `roll loop status` to falsely report off after `roll update`).
|
|
2292
|
+
local uid; uid=$(id -u)
|
|
2293
|
+
launchctl bootout "gui/${uid}/${label}" 2>/dev/null || true
|
|
2294
|
+
launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
|
|
2223
2295
|
fi
|
|
2224
2296
|
fi
|
|
2225
2297
|
done
|
|
@@ -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
|
-
|
|
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
|
|
3432
|
-
|
|
3433
|
-
|
|
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
|
-
|
|
3890
|
+
printf " Now: ${DIM:-}idle${NC}\n"
|
|
3436
3891
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
@@ -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
|