@seanyao/roll 2026.518.3 → 2026.518.4
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 +20 -0
- package/bin/roll +233 -92
- package/conventions/global/AGENTS.md +1 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/roll-backlog.py +262 -0
- package/lib/roll-brief.py +286 -0
- package/lib/roll-help.py +157 -0
- package/lib/roll-home.py +493 -0
- package/lib/roll-loop-status.py +16 -8
- package/lib/roll-setup.py +62 -0
- package/lib/roll-status.py +368 -0
- package/package.json +1 -1
- package/skills/roll-fix/SKILL.md +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.518.4
|
|
4
|
+
|
|
5
|
+
### Improved
|
|
6
|
+
|
|
7
|
+
- **`roll` 主页焕新** — 一屏看清 loop / dream / peer 状态、四道防线、Pipeline 进度和待你处理的事 `[loop]`
|
|
8
|
+
- **`roll --help` 焕新** — 命令按日常 / 项目 / 全局三组分类,常用命令 ★ 高亮,不再被大字 ASCII banner 占屏 `[loop]`
|
|
9
|
+
- **`roll status` 焕新** — 一行看健康总览,AI 客户端同步状态、约定文件清单、项目模板逐段展示,drift 行直接给修复命令 `[loop]`
|
|
10
|
+
- **`roll backlog`** — 待办任务按缺陷 / 故事 / 重构 / 创意四类分组显示,进行中的条目紫色高亮,Blocked / Deferred 分区附带原因 `[loop]`
|
|
11
|
+
- **`roll brief`** — 简报现在用终端三段式渲染:摘要数字、已完成亮点、待决策清单按 D1/D2 编号 `[loop]`
|
|
12
|
+
- **`roll setup`** — 首次安装流程现在显示 6 步编号进度,每步完成打勾,结尾显示"Setup complete" `[loop]`
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **loop 弹窗现在固定走 Terminal.app** — 不再按终端偏好挑来挑去,省得在新版 Ghostty 上弹窗假成功、备选方案也失效 `[loop]`
|
|
17
|
+
- **与 roll agent 对话** — 不再因工具不同而忽冷忽热 `[loop]`
|
|
18
|
+
- **大小写不同路径进同一项目** — 不再被 loop 当成两个独立项目、LOCK 和状态不再分裂 `[loop]`
|
|
19
|
+
- **loop 跑完一轮** — 不再挡死后续调度、卡到手动 kill `[loop]`
|
|
20
|
+
- **binary 升级后 loop dashboard 回落 IDLE 问题** — 旧 binary 用不同路径大小写算出不同 slug,新 binary 启动时自动把旧 slug 的状态文件、日志、历史记录全部迁到新 slug,upgrade 无感无损 `[loop]`
|
|
21
|
+
- **loop status 费用显示严重偏低** — `$9.25` 的 cycle 显示为 `$0.04`:读取了最后一次 API 调用的 token 数重新算价,改为直接用 `cost_reported_usd`(loop-fmt 写入的权威累计值) `[loop]`
|
|
22
|
+
|
|
3
23
|
## v2026.518.3
|
|
4
24
|
|
|
5
25
|
### Fixed
|
package/bin/roll
CHANGED
|
@@ -4,14 +4,14 @@ 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.518.
|
|
7
|
+
VERSION="2026.518.4"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
11
11
|
ROLL_TEMPLATES="${ROLL_HOME}/conventions/templates"
|
|
12
12
|
|
|
13
13
|
# Find package root (resolve symlinks so it works from ~/.local/bin/roll or npm global bin)
|
|
14
|
-
_source="${BASH_SOURCE[0]}"
|
|
14
|
+
_source="${BASH_SOURCE[0]:-$0}"
|
|
15
15
|
while [[ -L "$_source" ]]; do
|
|
16
16
|
_dir="$(cd "$(dirname "$_source")" && pwd)"
|
|
17
17
|
_source="$(readlink "$_source")"
|
|
@@ -574,13 +574,20 @@ _ensure_tmux() {
|
|
|
574
574
|
|
|
575
575
|
cmd_setup() {
|
|
576
576
|
local force=false
|
|
577
|
+
local demo=false
|
|
577
578
|
while [[ $# -gt 0 ]]; do
|
|
578
579
|
case "$1" in
|
|
579
580
|
--force|-f) force=true; shift ;;
|
|
581
|
+
--demo) demo=true; shift ;;
|
|
580
582
|
*) err "Unknown argument: $1 未知参数: $1"; exit 1 ;;
|
|
581
583
|
esac
|
|
582
584
|
done
|
|
583
585
|
|
|
586
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; then
|
|
587
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-setup.py" --demo
|
|
588
|
+
if [[ "$demo" == "true" ]]; then return; fi
|
|
589
|
+
fi
|
|
590
|
+
|
|
584
591
|
info "Setting up Roll on this machine... 正在初始化 Roll..."
|
|
585
592
|
echo ""
|
|
586
593
|
|
|
@@ -1070,7 +1077,7 @@ EOF
|
|
|
1070
1077
|
# COMMAND: status
|
|
1071
1078
|
# Show current state of conventions
|
|
1072
1079
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1073
|
-
|
|
1080
|
+
_legacy_status() {
|
|
1074
1081
|
echo -e "${BOLD}Roll Convention Status Roll 约定状态${NC}"
|
|
1075
1082
|
echo ""
|
|
1076
1083
|
|
|
@@ -1187,6 +1194,14 @@ cmd_status() {
|
|
|
1187
1194
|
_status_loop_overview
|
|
1188
1195
|
}
|
|
1189
1196
|
|
|
1197
|
+
cmd_status() {
|
|
1198
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
1199
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-status.py" "$@"
|
|
1200
|
+
else
|
|
1201
|
+
_legacy_status "$@"
|
|
1202
|
+
fi
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1190
1205
|
_status_loop_overview() {
|
|
1191
1206
|
[[ "$(uname)" != "Darwin" ]] && return 0
|
|
1192
1207
|
|
|
@@ -1352,41 +1367,20 @@ _peer_route() {
|
|
|
1352
1367
|
return 1
|
|
1353
1368
|
}
|
|
1354
1369
|
|
|
1355
|
-
# Open a
|
|
1356
|
-
# No-ops when muted, non-macOS, or
|
|
1370
|
+
# Open a Terminal.app window attached to the given tmux session (peer
|
|
1371
|
+
# auto-attach). No-ops when muted, non-macOS, or osascript unavailable.
|
|
1372
|
+
# FIX-054: terminal selection removed — always dispatches to macOS
|
|
1373
|
+
# Terminal.app for predictability (per-user detection silently failed on
|
|
1374
|
+
# Ghostty upgrades).
|
|
1357
1375
|
_peer_auto_attach() {
|
|
1358
1376
|
local session="$1"
|
|
1359
1377
|
[ "$(uname)" = "Darwin" ] || return 0
|
|
1360
1378
|
[ -f "$_LOOP_MUTE_FILE" ] && return 0
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
iTerm.app) terminal_pref="iTerm2" ;;
|
|
1367
|
-
*) terminal_pref="Terminal" ;;
|
|
1368
|
-
esac
|
|
1369
|
-
fi
|
|
1370
|
-
local launched=0
|
|
1371
|
-
if [[ "$terminal_pref" = "ghostty" || "$terminal_pref" = "Ghostty" ]]; then
|
|
1372
|
-
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1373
|
-
fi
|
|
1374
|
-
if [[ $launched -eq 0 ]] && { [[ "$terminal_pref" = "iTerm2" || "$terminal_pref" = "iTerm" ]] || [[ -d "/Applications/iTerm.app" ]]; }; then
|
|
1375
|
-
osascript \
|
|
1376
|
-
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \
|
|
1377
|
-
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t $session\"" \
|
|
1378
|
-
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \
|
|
1379
|
-
&& launched=1 || true
|
|
1380
|
-
fi
|
|
1381
|
-
if [[ $launched -eq 0 ]] && [[ -d "/Applications/Ghostty.app" ]]; then
|
|
1382
|
-
open -na Ghostty.app --args -e tmux attach -t "$session" >/dev/null 2>&1 && launched=1 || true
|
|
1383
|
-
fi
|
|
1384
|
-
if [[ $launched -eq 0 ]] && command -v osascript >/dev/null 2>&1; then
|
|
1385
|
-
osascript \
|
|
1386
|
-
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \
|
|
1387
|
-
-e "tell application \"Terminal\" to do script \"tmux attach -t $session\"" \
|
|
1388
|
-
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
1389
|
-
fi
|
|
1379
|
+
command -v osascript >/dev/null 2>&1 || return 0
|
|
1380
|
+
osascript \
|
|
1381
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \
|
|
1382
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t $session\"" \
|
|
1383
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
1390
1384
|
}
|
|
1391
1385
|
|
|
1392
1386
|
# Dispatch a peer CLI command inside an existing tmux session (window 0).
|
|
@@ -2005,6 +1999,13 @@ cmd_agent() {
|
|
|
2005
1999
|
# hash of the full path, ensuring uniqueness across sibling dirs with same name.
|
|
2006
2000
|
_project_slug() {
|
|
2007
2001
|
local path="${1:-$(pwd -P 2>/dev/null || pwd)}"
|
|
2002
|
+
# FIX-056: normalize path to canonical case on macOS case-insensitive filesystem.
|
|
2003
|
+
# Two paths differing only in case point to the same directory; realpath
|
|
2004
|
+
# resolves both symlinks and case variations to the canonical filesystem path.
|
|
2005
|
+
if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
|
|
2006
|
+
local _canon
|
|
2007
|
+
_canon=$(realpath "$path" 2>/dev/null) && path="$_canon"
|
|
2008
|
+
fi
|
|
2008
2009
|
# FIX-034: when inside a git worktree, git-common-dir returns the main tree's
|
|
2009
2010
|
# absolute .git path; resolve to the main tree so worktree and main-tree runs
|
|
2010
2011
|
# produce the same slug.
|
|
@@ -2024,6 +2025,90 @@ _project_slug() {
|
|
|
2024
2025
|
printf '%s' "${base}-${hash}"
|
|
2025
2026
|
}
|
|
2026
2027
|
|
|
2028
|
+
# FIX-058: migrate loop state files when the per-project slug changed due to
|
|
2029
|
+
# FIX-056 (realpath case-normalization on macOS). Called by
|
|
2030
|
+
# _install_launchd_plists before generating new runner/plist so existing state
|
|
2031
|
+
# (paused/running/etc.) is not silently lost.
|
|
2032
|
+
#
|
|
2033
|
+
# Usage: _slug_migrate_from_legacy <new_slug> [<loop_dir>] [<old_slug>]
|
|
2034
|
+
# new_slug — the correct slug computed by the current _project_slug
|
|
2035
|
+
# loop_dir — optional override of ${_SHARED_ROOT}/loop (for unit tests)
|
|
2036
|
+
# old_slug — optional explicit old slug (for unit tests; auto-computed otherwise)
|
|
2037
|
+
_slug_migrate_from_legacy() {
|
|
2038
|
+
local new_slug="$1"
|
|
2039
|
+
local loop_dir="${2:-${_SHARED_ROOT}/loop}"
|
|
2040
|
+
local old_slug="${3:-}"
|
|
2041
|
+
|
|
2042
|
+
if [[ -z "$old_slug" ]]; then
|
|
2043
|
+
[[ "$(uname -s 2>/dev/null)" == "Darwin" ]] || return 0
|
|
2044
|
+
# Compute the pre-FIX-056 slug: same algorithm but without realpath.
|
|
2045
|
+
local raw_path; raw_path=$(pwd 2>/dev/null)
|
|
2046
|
+
local _common
|
|
2047
|
+
_common=$(git -C "$raw_path" rev-parse --git-common-dir 2>/dev/null)
|
|
2048
|
+
if [[ -n "$_common" && "$_common" == *"/.git" ]]; then
|
|
2049
|
+
raw_path="${_common%/.git}"
|
|
2050
|
+
fi
|
|
2051
|
+
local old_base; old_base=$(basename "$raw_path")
|
|
2052
|
+
local old_hash
|
|
2053
|
+
if command -v md5 &>/dev/null; then
|
|
2054
|
+
old_hash=$(printf '%s' "$raw_path" | md5 | cut -c1-6)
|
|
2055
|
+
else
|
|
2056
|
+
old_hash=$(printf '%s' "$raw_path" | md5sum | cut -c1-6)
|
|
2057
|
+
fi
|
|
2058
|
+
old_base=$(printf '%s' "$old_base" | tr -cs '[:alnum:]' '-' | sed 's/-*$//')
|
|
2059
|
+
old_slug="${old_base}-${old_hash}"
|
|
2060
|
+
fi
|
|
2061
|
+
|
|
2062
|
+
[[ "$old_slug" == "$new_slug" ]] && return 0
|
|
2063
|
+
[[ -f "${loop_dir}/state-${old_slug}.yaml" ]] || return 0
|
|
2064
|
+
|
|
2065
|
+
printf 'roll: migrating loop state %s → %s\n' "$old_slug" "$new_slug" >&2
|
|
2066
|
+
|
|
2067
|
+
mv "${loop_dir}/state-${old_slug}.yaml" "${loop_dir}/state-${new_slug}.yaml"
|
|
2068
|
+
|
|
2069
|
+
[[ -f "${loop_dir}/cron-${old_slug}.log" ]] && \
|
|
2070
|
+
mv "${loop_dir}/cron-${old_slug}.log" "${loop_dir}/cron-${new_slug}.log"
|
|
2071
|
+
|
|
2072
|
+
if [[ -f "${loop_dir}/events-${old_slug}.ndjson" ]]; then
|
|
2073
|
+
if [[ -f "${loop_dir}/events-${new_slug}.ndjson" ]]; then
|
|
2074
|
+
cat "${loop_dir}/events-${old_slug}.ndjson" >> "${loop_dir}/events-${new_slug}.ndjson"
|
|
2075
|
+
rm "${loop_dir}/events-${old_slug}.ndjson"
|
|
2076
|
+
else
|
|
2077
|
+
mv "${loop_dir}/events-${old_slug}.ndjson" "${loop_dir}/events-${new_slug}.ndjson"
|
|
2078
|
+
fi
|
|
2079
|
+
fi
|
|
2080
|
+
|
|
2081
|
+
local runs_file="${loop_dir}/runs.jsonl"
|
|
2082
|
+
if [[ -f "$runs_file" ]]; then
|
|
2083
|
+
local tmp; tmp=$(mktemp)
|
|
2084
|
+
python3 - "$old_slug" "$new_slug" "$runs_file" > "$tmp" << 'PYEOF'
|
|
2085
|
+
import json, sys
|
|
2086
|
+
old, new, path = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
2087
|
+
with open(path) as f:
|
|
2088
|
+
for line in f:
|
|
2089
|
+
line = line.rstrip('\n')
|
|
2090
|
+
if not line:
|
|
2091
|
+
continue
|
|
2092
|
+
try:
|
|
2093
|
+
d = json.loads(line)
|
|
2094
|
+
if 'project' in d and old in str(d['project']):
|
|
2095
|
+
d['project'] = str(d['project']).replace(old, new)
|
|
2096
|
+
print(json.dumps(d))
|
|
2097
|
+
except Exception:
|
|
2098
|
+
print(line)
|
|
2099
|
+
PYEOF
|
|
2100
|
+
mv "$tmp" "$runs_file"
|
|
2101
|
+
fi
|
|
2102
|
+
|
|
2103
|
+
local old_plist=~/Library/LaunchAgents/com.roll.loop.${old_slug}.plist
|
|
2104
|
+
if [[ -f "$old_plist" ]]; then
|
|
2105
|
+
launchctl unload "$old_plist" 2>/dev/null || true
|
|
2106
|
+
rm -f "$old_plist"
|
|
2107
|
+
fi
|
|
2108
|
+
|
|
2109
|
+
rm -f "${loop_dir}/run-${old_slug}.sh" "${loop_dir}/run-${old_slug}-inner.sh"
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2027
2112
|
_LOOP_TAG="# roll-loop"
|
|
2028
2113
|
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
2029
2114
|
# FIX-052: per-project loop state — ALERT/state/mute were globally shared,
|
|
@@ -2202,7 +2287,10 @@ _write_runner_script() {
|
|
|
2202
2287
|
# Falls back to headless execution when tmux is not installed.
|
|
2203
2288
|
_write_loop_runner_script() {
|
|
2204
2289
|
local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
|
|
2205
|
-
local active_start="${5:-10}" active_end="${6:-18}"
|
|
2290
|
+
local active_start="${5:-10}" active_end="${6:-18}"
|
|
2291
|
+
# FIX-054: terminal preference detection removed. Popup is hard-coded to
|
|
2292
|
+
# macOS Terminal.app; the 7th positional arg, if any, is ignored for
|
|
2293
|
+
# backwards compatibility with existing callers.
|
|
2206
2294
|
mkdir -p "$(dirname "$script_path")"
|
|
2207
2295
|
|
|
2208
2296
|
local inner_path="${script_path%.sh}-inner.sh"
|
|
@@ -2262,7 +2350,29 @@ _heartbeat_writer() {
|
|
|
2262
2350
|
}
|
|
2263
2351
|
_heartbeat_writer &
|
|
2264
2352
|
_HEARTBEAT_PID=\$!
|
|
2265
|
-
|
|
2353
|
+
# FIX-057: cycle hard timeout — 45 minute SLA per loop cycle. If a cycle runs
|
|
2354
|
+
# longer, kill claude / loop-fmt.py / all backgrounded children, mark the
|
|
2355
|
+
# in-progress backlog item Blocked (caller decides), and exit cleanly so the
|
|
2356
|
+
# next cron tick can proceed. Overridable via env for tests.
|
|
2357
|
+
LOOP_CYCLE_TIMEOUT_SEC="\${ROLL_LOOP_CYCLE_TIMEOUT_SEC:-2700}"
|
|
2358
|
+
_CYCLE_TIMED_OUT=0
|
|
2359
|
+
_on_sigterm() { _CYCLE_TIMED_OUT=1; }
|
|
2360
|
+
trap '_on_sigterm' TERM
|
|
2361
|
+
_inner_cleanup() {
|
|
2362
|
+
local _rc=\$?
|
|
2363
|
+
# Kill heartbeat + every remaining background job (watchdog, orphan
|
|
2364
|
+
# loop-fmt.py, publish subshells) — bash's foreground 'wait' for the
|
|
2365
|
+
# pipe has already returned by the time the EXIT trap runs.
|
|
2366
|
+
kill "\${_HEARTBEAT_PID}" 2>/dev/null
|
|
2367
|
+
for _pid in \$(jobs -p); do kill "\$_pid" 2>/dev/null; done
|
|
2368
|
+
if [ "\${_CYCLE_TIMED_OUT:-0}" -eq 1 ]; then
|
|
2369
|
+
_loop_event cycle_end "\${CYCLE_ID:-unknown}" "\${BRANCH:-}" "blocked" 2>/dev/null || true
|
|
2370
|
+
_worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
|
|
2371
|
+
fi
|
|
2372
|
+
rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
|
|
2373
|
+
exit "\$_rc"
|
|
2374
|
+
}
|
|
2375
|
+
trap '_inner_cleanup' EXIT
|
|
2266
2376
|
|
|
2267
2377
|
# US-AUTO-037: pull in worktree helpers (US-AUTO-036). Sourcing bin/roll is
|
|
2268
2378
|
# safe — its main() only runs when invoked directly (BASH_SOURCE == \$0).
|
|
@@ -2350,12 +2460,21 @@ export LOOP_PROJECT_SLUG="${slug}"
|
|
|
2350
2460
|
export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
2351
2461
|
export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
2352
2462
|
for _attempt in 1 2 3; do
|
|
2463
|
+
# FIX-057: watchdog — fires SIGTERM at the inner script (and its direct
|
|
2464
|
+
# children) when the cycle exceeds LOOP_CYCLE_TIMEOUT_SEC. Signal the inner
|
|
2465
|
+
# script first so _on_sigterm sets _CYCLE_TIMED_OUT before pkill takes out
|
|
2466
|
+
# the watchdog subshell itself (pkill -P \$\$ matches this subshell too).
|
|
2467
|
+
( sleep "\$LOOP_CYCLE_TIMEOUT_SEC" && { kill -TERM \$\$ 2>/dev/null; pkill -TERM -P \$\$ 2>/dev/null; } ) &
|
|
2468
|
+
_WATCHDOG_PID=\$!
|
|
2353
2469
|
if [ -f "\$FMT" ]; then
|
|
2354
2470
|
( cd "\$WT" && ${claude_cmd} ) | python3 "\$FMT"
|
|
2355
2471
|
else
|
|
2356
2472
|
( cd "\$WT" && ${claude_cmd} )
|
|
2357
2473
|
fi
|
|
2358
2474
|
_exit=\$?
|
|
2475
|
+
kill "\$_WATCHDOG_PID" 2>/dev/null
|
|
2476
|
+
wait "\$_WATCHDOG_PID" 2>/dev/null
|
|
2477
|
+
[ "\$_CYCLE_TIMED_OUT" -eq 1 ] && break
|
|
2359
2478
|
[ "\$_exit" -eq 0 ] && break
|
|
2360
2479
|
if [ "\$_attempt" -lt 3 ]; then
|
|
2361
2480
|
echo "[loop] claude exited \$_exit (attempt \$_attempt/3) — retrying in 30s..."
|
|
@@ -2363,6 +2482,12 @@ for _attempt in 1 2 3; do
|
|
|
2363
2482
|
fi
|
|
2364
2483
|
done
|
|
2365
2484
|
|
|
2485
|
+
# FIX-057: timed out — skip publish; EXIT trap writes cycle_end blocked + ALERT.
|
|
2486
|
+
if [ "\$_CYCLE_TIMED_OUT" -eq 1 ]; then
|
|
2487
|
+
echo "[loop] cycle \${CYCLE_ID}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — aborting cycle (worktree preserved at \$WT)"
|
|
2488
|
+
exit 0
|
|
2489
|
+
fi
|
|
2490
|
+
|
|
2366
2491
|
# FIX-044: capture cycle data from worktree before cleanup removes it
|
|
2367
2492
|
_cycle_tcr=0
|
|
2368
2493
|
_cycle_status="idle"
|
|
@@ -2587,33 +2712,28 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
2587
2712
|
done
|
|
2588
2713
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
2589
2714
|
tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
|
|
2590
|
-
# Auto-attach popup: when not muted, spawn a Terminal window attached
|
|
2591
|
-
# tmux session so the user can watch the loop work in real time.
|
|
2592
|
-
#
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
iTerm2|iTerm)
|
|
2602
|
-
osascript \\
|
|
2603
|
-
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2604
|
-
-e "tell application \"iTerm2\" to create window with default profile command \"tmux attach -t \$SESSION\"" \\
|
|
2605
|
-
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 \\
|
|
2606
|
-
&& _launched=1 || true
|
|
2607
|
-
;;
|
|
2608
|
-
esac
|
|
2609
|
-
if [ "\$_launched" -eq 0 ] && command -v osascript >/dev/null 2>&1; then
|
|
2610
|
-
osascript \\
|
|
2611
|
-
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2612
|
-
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
2613
|
-
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
2614
|
-
fi
|
|
2715
|
+
# Auto-attach popup: when not muted, spawn a Terminal.app window attached
|
|
2716
|
+
# to the tmux session so the user can watch the loop work in real time.
|
|
2717
|
+
# FIX-054: terminal selection removed — fixed to macOS Terminal.app for
|
|
2718
|
+
# predictability (per-user detection silently failed on Ghostty upgrades).
|
|
2719
|
+
# Best-effort focus retention: capture the current frontmost app and
|
|
2720
|
+
# re-activate after.
|
|
2721
|
+
if [ -z "\${ROLL_LOOP_NO_POPUP:-}" ] && [ -z "\${BATS_TEST_NUMBER:-}" ] && [ ! -f "\$HOME/.shared/roll/loop/mute-${slug}" ] && [ "\$(uname)" = "Darwin" ] && command -v osascript >/dev/null 2>&1; then
|
|
2722
|
+
osascript \\
|
|
2723
|
+
-e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
|
|
2724
|
+
-e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
|
|
2725
|
+
-e 'delay 0.3' -e 'try' -e 'tell application _prev to activate' -e 'end try' >/dev/null 2>&1 || true
|
|
2615
2726
|
fi
|
|
2616
|
-
|
|
2727
|
+
_OUTER_TIMEOUT=\$(( \${ROLL_LOOP_CYCLE_TIMEOUT_SEC:-2700} + 300 ))
|
|
2728
|
+
_outer_wait_start=\$(date +%s)
|
|
2729
|
+
while tmux has-session -t "\$SESSION" 2>/dev/null; do
|
|
2730
|
+
sleep 5
|
|
2731
|
+
if (( \$(date +%s) - _outer_wait_start > _OUTER_TIMEOUT )); then
|
|
2732
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] FIX-057: outer timeout (\${_OUTER_TIMEOUT}s) — killing tmux session \$SESSION" >> "\$LOG"
|
|
2733
|
+
tmux kill-session -t "\$SESSION" 2>/dev/null || true
|
|
2734
|
+
break
|
|
2735
|
+
fi
|
|
2736
|
+
done
|
|
2617
2737
|
else
|
|
2618
2738
|
bash "\$INNER_SCRIPT" >> "\$LOG" 2>&1
|
|
2619
2739
|
fi
|
|
@@ -2659,16 +2779,7 @@ _install_launchd_plists() {
|
|
|
2659
2779
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
2660
2780
|
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
2661
2781
|
|
|
2662
|
-
#
|
|
2663
|
-
local terminal_pref
|
|
2664
|
-
terminal_pref=$(config_get "loop_attach_terminal" "")
|
|
2665
|
-
if [[ -z "$terminal_pref" ]]; then
|
|
2666
|
-
case "${TERM_PROGRAM:-}" in
|
|
2667
|
-
ghostty) terminal_pref="ghostty" ;;
|
|
2668
|
-
iTerm.app) terminal_pref="iTerm2" ;;
|
|
2669
|
-
*) terminal_pref="Terminal" ;;
|
|
2670
|
-
esac
|
|
2671
|
-
fi
|
|
2782
|
+
# FIX-054: terminal preference removed — runner always uses Terminal.app.
|
|
2672
2783
|
|
|
2673
2784
|
local services=("loop" "dream" "brief")
|
|
2674
2785
|
local skill_names=("roll-loop" "roll-.dream" "roll-brief")
|
|
@@ -2677,6 +2788,9 @@ _install_launchd_plists() {
|
|
|
2677
2788
|
|
|
2678
2789
|
local updated=0
|
|
2679
2790
|
local slug; slug=$(_project_slug "$project_path")
|
|
2791
|
+
# FIX-058: after FIX-056 introduced realpath normalization, the slug for an
|
|
2792
|
+
# existing project may have changed. Migrate state before creating new files.
|
|
2793
|
+
_slug_migrate_from_legacy "$slug" "${shared}/loop"
|
|
2680
2794
|
for i in "${!services[@]}"; do
|
|
2681
2795
|
local svc="${services[$i]}"
|
|
2682
2796
|
local skill="${skill_names[$i]}"
|
|
@@ -2690,7 +2804,7 @@ _install_launchd_plists() {
|
|
|
2690
2804
|
local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
|
|
2691
2805
|
|
|
2692
2806
|
if [[ "$svc" == "loop" ]]; then
|
|
2693
|
-
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
|
|
2807
|
+
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
|
|
2694
2808
|
else
|
|
2695
2809
|
_write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
|
|
2696
2810
|
fi
|
|
@@ -2709,6 +2823,13 @@ _install_launchd_plists() {
|
|
|
2709
2823
|
local uid; uid=$(id -u)
|
|
2710
2824
|
launchctl bootout "gui/${uid}/${label}" 2>/dev/null || true
|
|
2711
2825
|
launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
|
|
2826
|
+
elif [[ -z "$before" ]]; then
|
|
2827
|
+
# FIX-059: brand-new plist — macOS FSEvents auto-bootstraps any new
|
|
2828
|
+
# file dropped in ~/Library/LaunchAgents/, so projects never enabled
|
|
2829
|
+
# via 'roll loop on' would fire every hour. Immediately mark disabled
|
|
2830
|
+
# in the overrides db to block that auto-load.
|
|
2831
|
+
local uid; uid=$(id -u)
|
|
2832
|
+
launchctl disable "gui/${uid}/${label}" 2>/dev/null || true
|
|
2712
2833
|
fi
|
|
2713
2834
|
fi
|
|
2714
2835
|
done
|
|
@@ -2934,17 +3055,7 @@ _loop_test() {
|
|
|
2934
3055
|
local log="${_SHARED_ROOT}/loop/cron-${slug}.log"
|
|
2935
3056
|
local test_runner="${_SHARED_ROOT}/loop/run-${slug}-test.sh"
|
|
2936
3057
|
|
|
2937
|
-
#
|
|
2938
|
-
local terminal_pref
|
|
2939
|
-
terminal_pref=$(config_get "loop_attach_terminal" "")
|
|
2940
|
-
if [[ -z "$terminal_pref" ]]; then
|
|
2941
|
-
case "${TERM_PROGRAM:-}" in
|
|
2942
|
-
ghostty) terminal_pref="ghostty" ;;
|
|
2943
|
-
iTerm.app) terminal_pref="iTerm2" ;;
|
|
2944
|
-
*) terminal_pref="Terminal" ;;
|
|
2945
|
-
esac
|
|
2946
|
-
fi
|
|
2947
|
-
|
|
3058
|
+
# FIX-054: terminal preference removed — runner always uses Terminal.app.
|
|
2948
3059
|
local active_start active_end
|
|
2949
3060
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
2950
3061
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
@@ -2952,7 +3063,7 @@ _loop_test() {
|
|
|
2952
3063
|
info "Generating test runner... 正在生成测试启动脚本..."
|
|
2953
3064
|
_write_loop_runner_script "$test_runner" "$project_path" \
|
|
2954
3065
|
'claude -p "Reply with a single word: hello"; sleep 10' \
|
|
2955
|
-
"$log" "$active_start" "$active_end"
|
|
3066
|
+
"$log" "$active_start" "$active_end"
|
|
2956
3067
|
|
|
2957
3068
|
info "Starting smoke test (real claude, trivial prompt)... 正在运行 smoke 测试(真实 claude)..."
|
|
2958
3069
|
info "Watch for: tmux session + terminal popup + stream-json events flowing"
|
|
@@ -4057,7 +4168,7 @@ _changelog_audit_bullet() {
|
|
|
4057
4168
|
# Rule 1: length cap 30 (user-command in backticks bypasses this rule).
|
|
4058
4169
|
if ! printf '%s' "$bullet" | grep -q '`'; then
|
|
4059
4170
|
local len
|
|
4060
|
-
len=$(
|
|
4171
|
+
len=$(printf '%s' "$bullet" | LC_ALL=C.UTF-8 wc -m | tr -d ' ')
|
|
4061
4172
|
if [ "${len:-0}" -gt 30 ]; then
|
|
4062
4173
|
echo "over-length-30"
|
|
4063
4174
|
fi
|
|
@@ -4080,7 +4191,7 @@ _changelog_audit_bullet() {
|
|
|
4080
4191
|
fi
|
|
4081
4192
|
|
|
4082
4193
|
# Rule 5: required shape — em dash, 不再, or 现在.
|
|
4083
|
-
if ! printf '%s' "$bullet" | grep -qE '—|不再|现在'; then
|
|
4194
|
+
if ! printf '%s' "$bullet" | LC_ALL=C.UTF-8 grep -qE '—|不再|现在'; then
|
|
4084
4195
|
echo "bad-shape"
|
|
4085
4196
|
fi
|
|
4086
4197
|
|
|
@@ -4654,7 +4765,17 @@ cmd_brief() {
|
|
|
4654
4765
|
fi
|
|
4655
4766
|
fi
|
|
4656
4767
|
|
|
4657
|
-
[[ -f "$latest" ]]
|
|
4768
|
+
if [[ ! -f "$latest" ]]; then
|
|
4769
|
+
return 1
|
|
4770
|
+
fi
|
|
4771
|
+
|
|
4772
|
+
# ── Display mode ──────────────────────────────────────────────────────────
|
|
4773
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
4774
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-brief.py" "$@"
|
|
4775
|
+
return
|
|
4776
|
+
fi
|
|
4777
|
+
|
|
4778
|
+
cat "$latest"
|
|
4658
4779
|
}
|
|
4659
4780
|
|
|
4660
4781
|
_promote_unreleased() {
|
|
@@ -4861,6 +4982,10 @@ cmd_backlog() {
|
|
|
4861
4982
|
esac
|
|
4862
4983
|
|
|
4863
4984
|
# ── Display mode ──────────────────────────────────────────────────────────
|
|
4985
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
4986
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-backlog.py" "$@"
|
|
4987
|
+
return
|
|
4988
|
+
fi
|
|
4864
4989
|
local DIM='\033[2m'
|
|
4865
4990
|
|
|
4866
4991
|
local us_items fix_items refactor_items idea_items total=0
|
|
@@ -5150,7 +5275,7 @@ _dash_brief_summary() {
|
|
|
5150
5275
|
' "$latest" 2>/dev/null | head -1 | cut -c1-60
|
|
5151
5276
|
}
|
|
5152
5277
|
|
|
5153
|
-
|
|
5278
|
+
_legacy_home() {
|
|
5154
5279
|
local project_path; project_path=$(pwd -P)
|
|
5155
5280
|
local project_name; project_name=$(basename "$project_path")
|
|
5156
5281
|
local agent; agent=$(_project_agent)
|
|
@@ -5331,10 +5456,18 @@ _dashboard() {
|
|
|
5331
5456
|
echo ""
|
|
5332
5457
|
}
|
|
5333
5458
|
|
|
5459
|
+
_home() {
|
|
5460
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
5461
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-home.py" "$@"
|
|
5462
|
+
else
|
|
5463
|
+
_legacy_home "$@"
|
|
5464
|
+
fi
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5334
5467
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
5335
5468
|
# MAIN
|
|
5336
5469
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
5337
|
-
|
|
5470
|
+
_legacy_help() {
|
|
5338
5471
|
echo -e "${CYAN} ██████╗ ██████╗ ██╗ ██╗ ${NC}"
|
|
5339
5472
|
echo -e "${CYAN} ██╔══██╗██╔═══██╗██║ ██║ ${NC}"
|
|
5340
5473
|
echo -e "${CYAN} ██████╔╝██║ ██║██║ ██║ ${NC}"
|
|
@@ -5376,6 +5509,14 @@ usage() {
|
|
|
5376
5509
|
|
|
5377
5510
|
}
|
|
5378
5511
|
|
|
5512
|
+
_help() {
|
|
5513
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
5514
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-help.py" "$@"
|
|
5515
|
+
else
|
|
5516
|
+
_legacy_help "$@"
|
|
5517
|
+
fi
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5379
5520
|
main() {
|
|
5380
5521
|
local cmd="${1:-}"
|
|
5381
5522
|
shift || true
|
|
@@ -5395,8 +5536,8 @@ main() {
|
|
|
5395
5536
|
doctor) cmd_doctor "$@" ;;
|
|
5396
5537
|
review-pr) cmd_review_pr "$@" ;;
|
|
5397
5538
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
5398
|
-
help|--help|-h)
|
|
5399
|
-
"") [[ -f "BACKLOG.md" ]] &&
|
|
5539
|
+
help|--help|-h) _help "$@" ;;
|
|
5540
|
+
"") [[ -f "BACKLOG.md" ]] && _home || { _help; _show_changelog; } ;;
|
|
5400
5541
|
*)
|
|
5401
5542
|
err "Unknown command: $cmd 未知命令: $cmd"
|
|
5402
5543
|
echo ""
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- **Ambiguity resolution**: When user says "explicit" in automation contexts,
|
|
10
10
|
interpret as "logged/observable with clear output", NOT "requiring manual
|
|
11
11
|
intervention". Confirm with one question if uncertain.
|
|
12
|
+
- **Voice**: Natural, colleague-like tone — neither robotic ("Executing…") nor over-enthusiastic ("Great!"). "Done — here's what changed." instead of "Task completed successfully." Consistent warmth for success and failure alike.
|
|
12
13
|
- **Bilingual output**: EN + ZH on separate lines, never inline.
|
|
13
14
|
```
|
|
14
15
|
Processing...
|
|
Binary file
|
|
Binary file
|
|
Binary file
|