@seanyao/roll 2026.601.1 → 2026.601.2
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 +11 -0
- package/bin/roll +222 -17
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/i18n/peer.sh +7 -0
- package/lib/i18n/peer_help.sh +4 -0
- package/lib/roll-loop-status.py +46 -1
- package/lib/roll-peer.py +1 -1
- package/package.json +1 -1
- package/skills/roll-peer/SKILL.md +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.601.2
|
|
4
|
+
|
|
5
|
+
### 新功能
|
|
6
|
+
|
|
7
|
+
- **curl 安装器骨架:不靠 npm 也能装 roll(US-INSTALL-001)** — 自包含安装脚本探测 OS(仅放行 macOS / Linux)、preflight 检查 `bash≥3.2`/`python3`/`curl`/`tar` 缺啥报啥、把运行时装到 `~/.local/share/roll/` 并 symlink 进 PATH,重复运行即原地升级。本版先从本地源目录复制(真正的 `curl ... | bash` 远端取数留后续故事)`[loop]`
|
|
8
|
+
|
|
9
|
+
### 可见性
|
|
10
|
+
|
|
11
|
+
- **peer 评审可靠落盘、能查了(FIX-150a)** — 此前 peer 痕迹碎成三处且大多丢失;统一落盘到项目本地规范路径,新增查询命令翻看历次 peer 记录(发起方 / 对象 / 轮次 / 各方结论 / 耗时),不再依赖 agent 自觉写盘 `[loop]`
|
|
12
|
+
- **三个专用 loop(CI / PR / Alert)空闲时也留心跳(FIX-151)** — 健康空闲时不再零日志让人以为没在跑;每轮补一条轻量存活心跳,status 显示各 loop 上次运行距今多久 `[loop]`
|
|
13
|
+
|
|
3
14
|
## v2026.601.1
|
|
4
15
|
|
|
5
16
|
### 新功能
|
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.601.
|
|
7
|
+
VERSION="2026.601.2"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -3713,6 +3713,48 @@ _peer_ensure_state_dir() {
|
|
|
3713
3713
|
mkdir -p "${_PEER_STATE_DIR}/logs"
|
|
3714
3714
|
}
|
|
3715
3715
|
|
|
3716
|
+
# FIX-150a: project-local peer data directory (analogous to loop runtime dir).
|
|
3717
|
+
_peer_project_dir() {
|
|
3718
|
+
local proj
|
|
3719
|
+
proj=$(pwd -P 2>/dev/null || pwd)
|
|
3720
|
+
# FIX-056: normalize path to canonical case on macOS case-insensitive filesystem.
|
|
3721
|
+
if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
|
|
3722
|
+
local _canon
|
|
3723
|
+
_canon=$(realpath "$proj" 2>/dev/null) && proj="$_canon"
|
|
3724
|
+
fi
|
|
3725
|
+
# When inside a git worktree, resolve to main tree (same pattern as _project_slug).
|
|
3726
|
+
local _common
|
|
3727
|
+
_common=$(git -C "$proj" rev-parse --git-common-dir 2>/dev/null)
|
|
3728
|
+
if [[ -n "$_common" && "$_common" == *"/.git" ]]; then
|
|
3729
|
+
proj="${_common%/.git}"
|
|
3730
|
+
fi
|
|
3731
|
+
echo "${proj}/.roll/peer"
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
_peer_ensure_project_dir() {
|
|
3735
|
+
local dir
|
|
3736
|
+
dir=$(_peer_project_dir)
|
|
3737
|
+
mkdir -p "$dir/logs"
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
# FIX-150a: write a structured JSONL record to the project-local peer runs file.
|
|
3741
|
+
_peer_write_record() {
|
|
3742
|
+
local from_tool="$1"
|
|
3743
|
+
local to_tool="$2"
|
|
3744
|
+
local round="$3"
|
|
3745
|
+
local verdict="$4"
|
|
3746
|
+
local tag="$5"
|
|
3747
|
+
local duration_sec="$6"
|
|
3748
|
+
local dir
|
|
3749
|
+
dir=$(_peer_project_dir)
|
|
3750
|
+
mkdir -p "$dir"
|
|
3751
|
+
local ts
|
|
3752
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
3753
|
+
printf '{"ts":"%s","from":"%s","to":"%s","round":%s,"verdict":"%s","tag":"%s","duration_sec":%s}\n' \
|
|
3754
|
+
"$ts" "$from_tool" "$to_tool" "$round" "$verdict" "$tag" "$duration_sec" \
|
|
3755
|
+
>> "$dir/runs.jsonl"
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3716
3758
|
_peer_state_file() {
|
|
3717
3759
|
local pair="$1"
|
|
3718
3760
|
local key="$2"
|
|
@@ -3928,6 +3970,8 @@ cmd_peer() {
|
|
|
3928
3970
|
--yes|--yolo) yolo=true; shift ;;
|
|
3929
3971
|
status) subcmd="status"; shift ;;
|
|
3930
3972
|
reset) subcmd="reset"; shift; break ;;
|
|
3973
|
+
log) subcmd="log"; shift ;;
|
|
3974
|
+
runs) subcmd="runs"; shift ;;
|
|
3931
3975
|
help|--help|-h) subcmd="help"; shift ;;
|
|
3932
3976
|
*) err "$(msg peer.unknown_option_1)"; exit 1 ;;
|
|
3933
3977
|
esac
|
|
@@ -3936,6 +3980,8 @@ cmd_peer() {
|
|
|
3936
3980
|
case "$subcmd" in
|
|
3937
3981
|
status) cmd_peer_status; return ;;
|
|
3938
3982
|
reset) cmd_peer_reset "$@"; return ;;
|
|
3983
|
+
log) cmd_peer_log; return ;;
|
|
3984
|
+
runs) cmd_peer_runs "$@"; return ;;
|
|
3939
3985
|
help) cmd_peer_help; return ;;
|
|
3940
3986
|
esac
|
|
3941
3987
|
|
|
@@ -3985,6 +4031,9 @@ cmd_peer() {
|
|
|
3985
4031
|
fi
|
|
3986
4032
|
fi
|
|
3987
4033
|
|
|
4034
|
+
local start_epoch
|
|
4035
|
+
start_epoch=$(date +%s)
|
|
4036
|
+
|
|
3988
4037
|
local context=""
|
|
3989
4038
|
if [[ -n "$context_file" && -f "$context_file" ]]; then
|
|
3990
4039
|
context="$(cat "$context_file")"
|
|
@@ -4004,9 +4053,10 @@ cmd_peer() {
|
|
|
4004
4053
|
fi
|
|
4005
4054
|
fi
|
|
4006
4055
|
|
|
4007
|
-
|
|
4056
|
+
# FIX-150a: write logs to project-local path; keep global state dir for adaptive routing.
|
|
4057
|
+
_peer_ensure_project_dir
|
|
4008
4058
|
local log_file
|
|
4009
|
-
log_file="$
|
|
4059
|
+
log_file="$(_peer_project_dir)/logs/$(date +%Y%m%d_%H%M%S)_${from_tool}_${to_tool}.md"
|
|
4010
4060
|
{
|
|
4011
4061
|
echo "# Peer Review Log"
|
|
4012
4062
|
echo ""
|
|
@@ -4051,6 +4101,11 @@ cmd_peer() {
|
|
|
4051
4101
|
|
|
4052
4102
|
_peer_update_state "$pair" "$resolution"
|
|
4053
4103
|
|
|
4104
|
+
# FIX-150a: write structured record for observability.
|
|
4105
|
+
local duration_sec=0
|
|
4106
|
+
duration_sec=$(( $(date +%s) - start_epoch ))
|
|
4107
|
+
_peer_write_record "$from_tool" "$to_tool" "$round" "$resolution" "$tag" "$duration_sec"
|
|
4108
|
+
|
|
4054
4109
|
echo ""
|
|
4055
4110
|
echo -e "$(msg peer.peer_review_result_peer_review ${BOLD} ${NC})"
|
|
4056
4111
|
echo " Pair: $pair"
|
|
@@ -4185,10 +4240,72 @@ cmd_peer_help() {
|
|
|
4185
4240
|
echo ""
|
|
4186
4241
|
echo "Subcommands:"
|
|
4187
4242
|
echo "$(msg peer_help.status_show_peer_review_state)"
|
|
4243
|
+
echo "$(msg peer_help.log_show_latest_peer_transcript)"
|
|
4244
|
+
echo "$(msg peer_help.runs_show_recent_peer_review_runs)"
|
|
4188
4245
|
echo "$(msg peer_help.reset_pair_all_reset_peer_state)"
|
|
4189
4246
|
echo "$(msg peer_help.help_show_this_help)"
|
|
4190
4247
|
}
|
|
4191
4248
|
|
|
4249
|
+
# FIX-150a: `roll peer runs [N]` — show recent peer review runs (project-local).
|
|
4250
|
+
cmd_peer_runs() {
|
|
4251
|
+
local n=10
|
|
4252
|
+
while [[ $# -gt 0 ]]; do
|
|
4253
|
+
case "$1" in
|
|
4254
|
+
[0-9]*) n="$1"; shift ;;
|
|
4255
|
+
*) shift ;;
|
|
4256
|
+
esac
|
|
4257
|
+
done
|
|
4258
|
+
|
|
4259
|
+
local dir
|
|
4260
|
+
dir=$(_peer_project_dir)
|
|
4261
|
+
local runs_file="$dir/runs.jsonl"
|
|
4262
|
+
|
|
4263
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
4264
|
+
err "$(msg peer.jq_required_for_roll_peer_runs)"
|
|
4265
|
+
return 1
|
|
4266
|
+
fi
|
|
4267
|
+
|
|
4268
|
+
if [[ ! -f "$runs_file" ]] || [[ ! -s "$runs_file" ]]; then
|
|
4269
|
+
echo "$(msg peer.no_peer_runs_yet)"
|
|
4270
|
+
return 0
|
|
4271
|
+
fi
|
|
4272
|
+
|
|
4273
|
+
local reversed
|
|
4274
|
+
reversed=$(awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}' "$runs_file")
|
|
4275
|
+
local recent
|
|
4276
|
+
recent=$(printf '%s\n' "$reversed" | head -n "$n")
|
|
4277
|
+
|
|
4278
|
+
echo -e "${BOLD}Peer Review Runs${NC}"
|
|
4279
|
+
echo ""
|
|
4280
|
+
printf "%-19s %-8s %-10s %-5s %-10s %s\n" "Time" "From" "To" "Rnd" "Verdict" "Tag"
|
|
4281
|
+
printf "%s\n" "─────────────────── ──────── ────────── ───── ────────── ──────────"
|
|
4282
|
+
|
|
4283
|
+
while IFS= read -r line; do
|
|
4284
|
+
[[ -z "$line" ]] && continue
|
|
4285
|
+
local ts from to round verdict tag
|
|
4286
|
+
ts=$(printf '%s' "$line" | jq -r '.ts // "—"')
|
|
4287
|
+
from=$(printf '%s' "$line" | jq -r '.from // "—"')
|
|
4288
|
+
to=$(printf '%s' "$line" | jq -r '.to // "—"')
|
|
4289
|
+
round=$(printf '%s' "$line" | jq -r '.round // "—"')
|
|
4290
|
+
verdict=$(printf '%s' "$line" | jq -r '.verdict // "—"')
|
|
4291
|
+
tag=$(printf '%s' "$line" | jq -r '.tag // "—"')
|
|
4292
|
+
printf "%-19s %-8s %-10s %-5s %-10s %s\n" "$ts" "$from" "$to" "$round" "$verdict" "$tag"
|
|
4293
|
+
done <<<"$recent"
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
# FIX-150a: `roll peer log` — show the latest peer review transcript.
|
|
4297
|
+
cmd_peer_log() {
|
|
4298
|
+
local dir
|
|
4299
|
+
dir=$(_peer_project_dir)
|
|
4300
|
+
local latest
|
|
4301
|
+
latest=$(ls "$dir/logs"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
4302
|
+
if [[ -z "$latest" || ! -f "$latest" ]]; then
|
|
4303
|
+
echo "$(msg peer.no_peer_logs_found)"
|
|
4304
|
+
return 0
|
|
4305
|
+
fi
|
|
4306
|
+
cat "$latest"
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4192
4309
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4193
4310
|
# AGENT — per-project agent configuration
|
|
4194
4311
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -7615,6 +7732,84 @@ _loop_event_rotate() {
|
|
|
7615
7732
|
fi
|
|
7616
7733
|
}
|
|
7617
7734
|
|
|
7735
|
+
# FIX-151: write a lightweight tick heartbeat for dedicated loops (pr/ci/alert).
|
|
7736
|
+
# Appends one JSONL line per tick; rotates by line count to control bloat.
|
|
7737
|
+
_loop_write_tick() {
|
|
7738
|
+
local loop_type="${1:-}" outcome="${2:-idle}" note="${3:-}"
|
|
7739
|
+
[ -n "$loop_type" ] || return 0
|
|
7740
|
+
local slug tick_file
|
|
7741
|
+
slug=$(_project_slug 2>/dev/null || basename "$PWD")
|
|
7742
|
+
local _rt_dir
|
|
7743
|
+
_rt_dir=$(_loop_runtime_dir "$slug" 2>/dev/null || echo "")
|
|
7744
|
+
if [ -n "$_rt_dir" ]; then
|
|
7745
|
+
tick_file="${_rt_dir}/${loop_type}-tick.jsonl"
|
|
7746
|
+
else
|
|
7747
|
+
tick_file="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/${loop_type}-tick-${slug}.jsonl"
|
|
7748
|
+
fi
|
|
7749
|
+
mkdir -p "$(dirname "$tick_file")" 2>/dev/null || true
|
|
7750
|
+
local ts
|
|
7751
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
7752
|
+
printf '{"ts":"%s","loop":"%s","outcome":"%s","note":"%s"}\n' \
|
|
7753
|
+
"$ts" "$loop_type" "$outcome" "$note" >> "$tick_file"
|
|
7754
|
+
|
|
7755
|
+
# Rotate: alert loop (1 min) → 1000 lines (~16h); ci/pr (5 min) → 500 lines (~42h)
|
|
7756
|
+
local max_lines=500
|
|
7757
|
+
[ "$loop_type" = "alert" ] && max_lines=1000
|
|
7758
|
+
local line_count
|
|
7759
|
+
line_count=$(wc -l < "$tick_file" 2>/dev/null | tr -d ' \t' || echo 0)
|
|
7760
|
+
case "$line_count" in ''|*[!0-9]*) line_count=0 ;; esac
|
|
7761
|
+
if [ "$line_count" -gt "$max_lines" ]; then
|
|
7762
|
+
tail -n "$max_lines" "$tick_file" > "${tick_file}.tmp" && mv "${tick_file}.tmp" "$tick_file"
|
|
7763
|
+
fi
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7766
|
+
# FIX-151: read the last tick line from a dedicated loop's tick file.
|
|
7767
|
+
# Optional second arg selects a JSON field (ts, loop, outcome, note).
|
|
7768
|
+
_loop_read_last_tick() {
|
|
7769
|
+
local loop_type="${1:-}" field="${2:-}"
|
|
7770
|
+
[ -n "$loop_type" ] || return 0
|
|
7771
|
+
local slug tick_file
|
|
7772
|
+
slug=$(_project_slug 2>/dev/null || basename "$PWD")
|
|
7773
|
+
local _rt_dir
|
|
7774
|
+
_rt_dir=$(_loop_runtime_dir "$slug" 2>/dev/null || echo "")
|
|
7775
|
+
if [ -n "$_rt_dir" ]; then
|
|
7776
|
+
tick_file="${_rt_dir}/${loop_type}-tick.jsonl"
|
|
7777
|
+
else
|
|
7778
|
+
tick_file="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/${loop_type}-tick-${slug}.jsonl"
|
|
7779
|
+
fi
|
|
7780
|
+
[ -f "$tick_file" ] || return 0
|
|
7781
|
+
local last
|
|
7782
|
+
last=$(tail -1 "$tick_file" 2>/dev/null || echo "")
|
|
7783
|
+
[ -n "$last" ] || return 0
|
|
7784
|
+
if [ -n "$field" ]; then
|
|
7785
|
+
echo "$last" | awk -F'"' '{for(i=2;i<NF;i+=2) if($i=="'"$field"'") {print $(i+2); exit}}' 2>/dev/null || echo ""
|
|
7786
|
+
else
|
|
7787
|
+
printf '%s\n' "$last"
|
|
7788
|
+
fi
|
|
7789
|
+
}
|
|
7790
|
+
|
|
7791
|
+
# FIX-151: compute human-readable age of the last tick for a dedicated loop.
|
|
7792
|
+
# Prints something like "5s", "3m", "2h" or empty string if no tick.
|
|
7793
|
+
_loop_tick_age() {
|
|
7794
|
+
local loop_type="${1:-}"
|
|
7795
|
+
[ -n "$loop_type" ] || return 0
|
|
7796
|
+
local ts
|
|
7797
|
+
ts=$(_loop_read_last_tick "$loop_type" "ts")
|
|
7798
|
+
[ -n "$ts" ] || return 0
|
|
7799
|
+
local tick_epoch now_epoch age
|
|
7800
|
+
tick_epoch=$(date -d "$ts" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo 0)
|
|
7801
|
+
[ "$tick_epoch" -gt 0 ] || return 0
|
|
7802
|
+
now_epoch=$(date +%s)
|
|
7803
|
+
age=$(( now_epoch - tick_epoch ))
|
|
7804
|
+
if [ "$age" -lt 60 ]; then
|
|
7805
|
+
echo "${age}s"
|
|
7806
|
+
elif [ "$age" -lt 3600 ]; then
|
|
7807
|
+
echo "$(( age / 60 ))m"
|
|
7808
|
+
else
|
|
7809
|
+
echo "$(( age / 3600 ))h"
|
|
7810
|
+
fi
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7618
7813
|
# US-OBS-014: after a loop cycle reaches a terminal cycle_end (done or idle),
|
|
7619
7814
|
# fire a best-effort, background status-snapshot push to roll-meta so the
|
|
7620
7815
|
# remote-watch prompt always sees ≤35min-fresh data — no user-side cron needed.
|
|
@@ -9783,8 +9978,13 @@ _legacy_loop_status() {
|
|
|
9783
9978
|
fi
|
|
9784
9979
|
echo -e " ${YELLOW}loop ⏸ paused${NC}${_dur} run: roll loop resume"
|
|
9785
9980
|
else
|
|
9981
|
+
local _tick_age=""
|
|
9982
|
+
case "$svc" in pr|ci|alert)
|
|
9983
|
+
_tick_age=$(_loop_tick_age "$svc")
|
|
9984
|
+
[ -n "$_tick_age" ] && _tick_age=" tick ${_tick_age}"
|
|
9985
|
+
esac
|
|
9786
9986
|
case "$state" in
|
|
9787
|
-
enabled) echo -e " ${GREEN}${svc} ● enabled${NC}" ;;
|
|
9987
|
+
enabled) echo -e " ${GREEN}${svc} ● enabled${NC}${_tick_age}" ;;
|
|
9788
9988
|
stale|installed-off) echo -e " ${YELLOW}${svc} ⚠ STALE — plist present but not loaded${NC} run: roll loop on" ;;
|
|
9789
9989
|
not-installed) echo -e " ${RED}${svc} ○ not installed${NC} run: roll setup" ;;
|
|
9790
9990
|
esac
|
|
@@ -11193,16 +11393,16 @@ _loop_pr_merge_self_eager() {
|
|
|
11193
11393
|
# Walks open PRs and routes each by classification.
|
|
11194
11394
|
# Lenient on gh unavailability — returns 0 so the loop continues to BACKLOG.
|
|
11195
11395
|
_loop_pr_inbox() {
|
|
11196
|
-
local slug; _gh_resolve slug || return 0
|
|
11396
|
+
local slug; _gh_resolve slug || { _loop_write_tick "pr" "idle" "gh_unavailable"; return 0; }
|
|
11197
11397
|
local prs_json
|
|
11198
11398
|
prs_json=$(gh -R "$slug" pr list --state open \
|
|
11199
11399
|
--json number,headRefName,author,title \
|
|
11200
|
-
2>/dev/null) || return 0
|
|
11201
|
-
[ -n "$prs_json" ] || return 0
|
|
11202
|
-
[ "$prs_json" = "[]" ] && return 0
|
|
11400
|
+
2>/dev/null) || { _loop_write_tick "pr" "idle" "gh_error"; return 0; }
|
|
11401
|
+
[ -n "$prs_json" ] || { _loop_write_tick "pr" "idle" "empty_response"; return 0; }
|
|
11402
|
+
[ "$prs_json" = "[]" ] && { _loop_write_tick "pr" "idle" "no_open_prs"; return 0; }
|
|
11203
11403
|
|
|
11204
11404
|
local count; count=$(echo "$prs_json" | jq 'length' 2>/dev/null || echo 0)
|
|
11205
|
-
[ "${count:-0}" -gt 0 ] || return 0
|
|
11405
|
+
[ "${count:-0}" -gt 0 ] || { _loop_write_tick "pr" "idle" "zero_prs"; return 0; }
|
|
11206
11406
|
|
|
11207
11407
|
local i=0
|
|
11208
11408
|
while [ "$i" -lt "$count" ]; do
|
|
@@ -11270,6 +11470,7 @@ _loop_pr_inbox() {
|
|
|
11270
11470
|
|
|
11271
11471
|
i=$((i + 1))
|
|
11272
11472
|
done
|
|
11473
|
+
_loop_write_tick "pr" "acted" "inbox_done"
|
|
11273
11474
|
return 0
|
|
11274
11475
|
}
|
|
11275
11476
|
|
|
@@ -11778,13 +11979,13 @@ _ci_detect_degradation() {
|
|
|
11778
11979
|
# accumulated history. Lenient on gh unavailability (missing / failed list →
|
|
11779
11980
|
# return 0) so the service never errors out a tick.
|
|
11780
11981
|
_ci_scan() {
|
|
11781
|
-
local slug; _gh_resolve slug 2>/dev/null || return 0
|
|
11982
|
+
local slug; _gh_resolve slug 2>/dev/null || { _loop_write_tick "ci" "idle" "gh_unavailable"; return 0; }
|
|
11782
11983
|
|
|
11783
11984
|
local runs_json
|
|
11784
11985
|
runs_json=$(gh -R "$slug" run list --branch main \
|
|
11785
11986
|
--json databaseId,workflowName,name,conclusion,status,createdAt,updatedAt \
|
|
11786
|
-
2>/dev/null) || return 0
|
|
11787
|
-
[ -n "$runs_json" ] || return 0
|
|
11987
|
+
2>/dev/null) || { _loop_write_tick "ci" "idle" "gh_error"; return 0; }
|
|
11988
|
+
[ -n "$runs_json" ] || { _loop_write_tick "ci" "idle" "empty_response"; return 0; }
|
|
11788
11989
|
|
|
11789
11990
|
# An empty list ("[]") still falls through to the detectors below: they run
|
|
11790
11991
|
# over accumulated history, not just this tick's runs.
|
|
@@ -11810,6 +12011,7 @@ _ci_scan() {
|
|
|
11810
12011
|
|
|
11811
12012
|
_ci_detect_flaky
|
|
11812
12013
|
_ci_detect_degradation
|
|
12014
|
+
_loop_write_tick "ci" "acted" "scan_done"
|
|
11813
12015
|
return 0
|
|
11814
12016
|
}
|
|
11815
12017
|
|
|
@@ -11977,12 +12179,12 @@ _alert_rotate() {
|
|
|
11977
12179
|
# A missing/empty alert file is a no-op (no rotate, no log). Loop-safe.
|
|
11978
12180
|
_alert_dispatch() {
|
|
11979
12181
|
local file="${1:-$_LOOP_ALERT}"
|
|
11980
|
-
[ -n "$file" ] && [ -f "$file" ] || return 0
|
|
12182
|
+
[ -n "$file" ] && [ -f "$file" ] || { _loop_write_tick "alert" "idle" "no_file"; return 0; }
|
|
11981
12183
|
# Empty file → nothing to consume, leave it in place.
|
|
11982
|
-
[ -s "$file" ] || return 0
|
|
12184
|
+
[ -s "$file" ] || { _loop_write_tick "alert" "idle" "empty_file"; return 0; }
|
|
11983
12185
|
|
|
11984
12186
|
local parsed; parsed=$(_alert_parse_file "$file")
|
|
11985
|
-
[ -n "$parsed" ] || { _alert_rotate "$file"; return 0; }
|
|
12187
|
+
[ -n "$parsed" ] || { _alert_rotate "$file"; _loop_write_tick "alert" "idle" "no_parsed"; return 0; }
|
|
11986
12188
|
|
|
11987
12189
|
local line ts level category message notify
|
|
11988
12190
|
local _oifs="$IFS"
|
|
@@ -12008,6 +12210,7 @@ _alert_dispatch() {
|
|
|
12008
12210
|
IFS="$_oifs"
|
|
12009
12211
|
|
|
12010
12212
|
_alert_rotate "$file"
|
|
12213
|
+
_loop_write_tick "alert" "acted" "dispatch_done"
|
|
12011
12214
|
return 0
|
|
12012
12215
|
}
|
|
12013
12216
|
|
|
@@ -14399,9 +14602,11 @@ _dash_refactor_pending() {
|
|
|
14399
14602
|
|
|
14400
14603
|
# ② Peer layer: last result + days ago from peer log, empty if no log.
|
|
14401
14604
|
_dash_last_peer() {
|
|
14402
|
-
local
|
|
14605
|
+
# FIX-150a: read from project-local peer logs (was ~/.shared/roll/peer/*.log).
|
|
14606
|
+
local peer_log_dir
|
|
14607
|
+
peer_log_dir=$(_peer_project_dir)/logs
|
|
14403
14608
|
local latest
|
|
14404
|
-
latest=$(ls "$peer_log_dir"/*.
|
|
14609
|
+
latest=$(ls "$peer_log_dir"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
14405
14610
|
[[ -z "$latest" || ! -f "$latest" ]] && return 0
|
|
14406
14611
|
local result
|
|
14407
14612
|
result=$(grep -oE '(AGREE|REFINE|OBJECT|ESCALATE)' "$latest" 2>/dev/null | tail -1 || true)
|
|
Binary file
|
package/lib/i18n/peer.sh
CHANGED
|
@@ -32,3 +32,10 @@ _i18n_set en peer.en_peer_review "[EN:启动 peer review: %s → %s (第 %s 轮,
|
|
|
32
32
|
_i18n_set zh peer.en_peer_review "启动 peer review: %s → %s (第 %s 轮, tag: %s)"
|
|
33
33
|
_i18n_set en peer.en_enter_n "[EN:按 Enter 执行或输入 n 取消。%s 秒后自动执行......]"
|
|
34
34
|
_i18n_set zh peer.en_enter_n "按 Enter 执行或输入 n 取消。%s 秒后自动执行..."
|
|
35
|
+
|
|
36
|
+
_i18n_set en peer.no_peer_runs_yet "No peer review runs yet."
|
|
37
|
+
_i18n_set zh peer.no_peer_runs_yet "还没有 peer review 记录。"
|
|
38
|
+
_i18n_set en peer.no_peer_logs_found "No peer logs found."
|
|
39
|
+
_i18n_set zh peer.no_peer_logs_found "还没有 peer 日志。"
|
|
40
|
+
_i18n_set en peer.jq_required_for_roll_peer_runs "jq is required for 'roll peer runs'."
|
|
41
|
+
_i18n_set zh peer.jq_required_for_roll_peer_runs "'roll peer runs' 需要安装 jq。"
|
package/lib/i18n/peer_help.sh
CHANGED
|
@@ -15,6 +15,10 @@ _i18n_set en peer_help.yes_yolo_skip_opt_out_prompt " --yes, --yolo Skip
|
|
|
15
15
|
_i18n_set zh peer_help.yes_yolo_skip_opt_out_prompt "跳过确认提示"
|
|
16
16
|
_i18n_set en peer_help.status_show_peer_review_state " status Show peer review state"
|
|
17
17
|
_i18n_set zh peer_help.status_show_peer_review_state "显示状态"
|
|
18
|
+
_i18n_set en peer_help.log_show_latest_peer_transcript " log Show latest peer review transcript"
|
|
19
|
+
_i18n_set zh peer_help.log_show_latest_peer_transcript "查看最新 peer 日志"
|
|
20
|
+
_i18n_set en peer_help.runs_show_recent_peer_review_runs " runs [N] Show recent peer review runs"
|
|
21
|
+
_i18n_set zh peer_help.runs_show_recent_peer_review_runs "查看最近 peer review 记录"
|
|
18
22
|
_i18n_set en peer_help.reset_pair_all_reset_peer_state " reset <pair|--all> Reset peer state"
|
|
19
23
|
_i18n_set zh peer_help.reset_pair_all_reset_peer_state "重置状态"
|
|
20
24
|
_i18n_set en peer_help.help_show_this_help " help Show this help"
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -162,7 +162,11 @@ def _resolve_project_path(slug: str) -> Optional[Path]:
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
def _loop_runtime_dir_py(slug: str) -> Optional[Path]:
|
|
165
|
-
"""Mirror bin/roll's _loop_runtime_dir: return <project>/.roll/loop.
|
|
165
|
+
"""Mirror bin/roll's _loop_runtime_dir: return <project>/.roll/loop.
|
|
166
|
+
Honors ROLL_PROJECT_RUNTIME_DIR env override (test sandbox)."""
|
|
167
|
+
env_rt = os.environ.get("ROLL_PROJECT_RUNTIME_DIR", "").strip()
|
|
168
|
+
if env_rt:
|
|
169
|
+
return Path(env_rt)
|
|
166
170
|
proj = _resolve_project_path(slug)
|
|
167
171
|
if proj is None:
|
|
168
172
|
return None
|
|
@@ -1189,6 +1193,11 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
1189
1193
|
_sl = _daily_schedule_line(_svc, now=now)
|
|
1190
1194
|
if _sl:
|
|
1191
1195
|
print(" " + c("dim", _sl))
|
|
1196
|
+
# FIX-151: dedicated loop (pr/ci/alert) last-tick age
|
|
1197
|
+
for _loop in ("pr", "ci", "alert"):
|
|
1198
|
+
_tl = _tick_age_line(_loop, now=now)
|
|
1199
|
+
if _tl:
|
|
1200
|
+
print(" " + c("dim", _tl))
|
|
1192
1201
|
print()
|
|
1193
1202
|
|
|
1194
1203
|
print(c("faint", "─" * COLS))
|
|
@@ -1436,6 +1445,42 @@ def _daily_schedule_line(svc: str, now: Optional[datetime] = None) -> Optional[s
|
|
|
1436
1445
|
return f"{svc}: daily (legacy interval)"
|
|
1437
1446
|
|
|
1438
1447
|
|
|
1448
|
+
def _tick_age_line(loop_type: str, now: Optional[datetime] = None) -> Optional[str]:
|
|
1449
|
+
"""FIX-151: read the last tick for a dedicated loop (pr/ci/alert) and return
|
|
1450
|
+
a human-readable age line, or None if no tick file exists."""
|
|
1451
|
+
slug = project_slug()
|
|
1452
|
+
rt_dir = _loop_runtime_dir_py(slug)
|
|
1453
|
+
if rt_dir is not None:
|
|
1454
|
+
tick_file = rt_dir / f"{loop_type}-tick.jsonl"
|
|
1455
|
+
else:
|
|
1456
|
+
tick_file = shared_root() / "loop" / f"{loop_type}-tick-{slug}.jsonl"
|
|
1457
|
+
if not tick_file.exists():
|
|
1458
|
+
return None
|
|
1459
|
+
try:
|
|
1460
|
+
last_line = tick_file.read_text().strip().splitlines()[-1]
|
|
1461
|
+
except (IndexError, OSError):
|
|
1462
|
+
return None
|
|
1463
|
+
# Extract ts field from JSONL
|
|
1464
|
+
m = re.search(r'"ts":"([^"]+)"', last_line)
|
|
1465
|
+
if not m:
|
|
1466
|
+
return None
|
|
1467
|
+
ts_str = m.group(1)
|
|
1468
|
+
try:
|
|
1469
|
+
# Parse ISO 8601 UTC timestamp
|
|
1470
|
+
tick_dt = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
1471
|
+
except ValueError:
|
|
1472
|
+
return None
|
|
1473
|
+
base = now or datetime.now(timezone.utc)
|
|
1474
|
+
age_sec = int((base - tick_dt).total_seconds())
|
|
1475
|
+
if age_sec < 60:
|
|
1476
|
+
age_str = f"{age_sec}s"
|
|
1477
|
+
elif age_sec < 3600:
|
|
1478
|
+
age_str = f"{age_sec // 60}m"
|
|
1479
|
+
else:
|
|
1480
|
+
age_str = f"{age_sec // 3600}h"
|
|
1481
|
+
return f"{loop_type}: tick {age_str} ago"
|
|
1482
|
+
|
|
1483
|
+
|
|
1439
1484
|
def _detect_install_state() -> str:
|
|
1440
1485
|
"""FIX-095 / FIX-098: classify the launchd install state of the loop service.
|
|
1441
1486
|
|
package/lib/roll-peer.py
CHANGED
|
@@ -99,7 +99,7 @@ _FIXTURE_VERDICT = {
|
|
|
99
99
|
"reason": "2 rounds · 5 turns · all blocks resolved",
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
_FIXTURE_ARTIFACT = "
|
|
102
|
+
_FIXTURE_ARTIFACT = ".roll/peer/logs/20260519_213700_claude_codex.md"
|
|
103
103
|
_FIXTURE_NEXT = [
|
|
104
104
|
("Continue execution", "claude resumes work on US-AUTH-014"),
|
|
105
105
|
("Inspect log", "open the artifact above to replay the transcript"),
|
package/package.json
CHANGED
|
@@ -180,7 +180,7 @@ Running
|
|
|
180
180
|
|
|
181
181
|
### Per Peer Pair (e.g., kimi→claude)
|
|
182
182
|
|
|
183
|
-
Stored in `~/.
|
|
183
|
+
Stored in `~/.roll/.peer-state/` (flat key files per pair):
|
|
184
184
|
|
|
185
185
|
```yaml
|
|
186
186
|
kimi→claude:
|
|
@@ -269,7 +269,7 @@ When peer review is manually triggered by a human (via `/peer`, "叫上 peer", e
|
|
|
269
269
|
- Relay the peer's response **verbatim** before adding your own analysis.
|
|
270
270
|
- After the peer's reply, the reviewer's own analysis block must explicitly state whether the peer's root cause and fix direction match the reviewer's own (independent) conclusion — that comparison is what determines the next round's action.
|
|
271
271
|
- If a peer call fails or times out, report it immediately inline and either retry or ESCALATE.
|
|
272
|
-
- Negotiation log is
|
|
272
|
+
- Negotiation log is written to `<project>/.roll/peer/logs/` as usual.
|
|
273
273
|
|
|
274
274
|
**Why inline, not tmux:** When a human manually triggers peer review inside an agent's interactive session, the conversation IS the visible interface. tmux auto-attach is only relevant for CLI-launched background sessions (`bin/roll peer`), not for skill invocations.
|
|
275
275
|
|
|
@@ -300,8 +300,9 @@ When Attacker and Defender reach a stalemate (both tests pass but interpretation
|
|
|
300
300
|
|
|
301
301
|
## Output Artifacts
|
|
302
302
|
|
|
303
|
-
- **Negotiation log**:
|
|
304
|
-
- **
|
|
303
|
+
- **Negotiation log**: `<project>/.roll/peer/logs/<timestamp>_<from>_<to>.md`
|
|
304
|
+
- **Structured record**: `<project>/.roll/peer/runs.jsonl`
|
|
305
|
+
- **State file**: `~/.roll/.peer-state/`
|
|
305
306
|
- **Decision record**: If AGREE, append summary to `docs/decisions/` or `.roll/backlog.md` (optional)
|
|
306
307
|
|
|
307
308
|
## Configuration
|
|
@@ -327,7 +328,7 @@ peer:
|
|
|
327
328
|
|
|
328
329
|
## Limitations
|
|
329
330
|
|
|
330
|
-
1. **Reverse link reliability**: Direct CLI calls are preferred. Reliability varies by tool — see Peer Invocation Reference table. If a peer fails consistently, the adaptive streak tracker marks it `abandoned` and falls back to the next candidate. File mailbox (
|
|
331
|
+
1. **Reverse link reliability**: Direct CLI calls are preferred. Reliability varies by tool — see Peer Invocation Reference table. If a peer fails consistently, the adaptive streak tracker marks it `abandoned` and falls back to the next candidate. File mailbox (`<project>/.roll/peer/mailbox/`) is the last-resort fallback.
|
|
331
332
|
- `deepseek serve --http` is the most reliable option when available — prefer it over direct `deepseek` CLI invocation.
|
|
332
333
|
- `codex exec` has known TTY/Ink issues in non-interactive environments; treat as low-priority fallback.
|
|
333
334
|
2. **Cost**: Every peer review consumes tokens on both sides. Only trigger for tasks where the cost of a wrong decision exceeds the cost of peer review. DeepSeek is the most cost-effective peer for general use.
|