@seanyao/roll 2026.524.1 → 2026.524.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 +13 -0
- package/bin/roll +137 -24
- package/lib/__pycache__/loop-fmt.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/loop-fmt.py +65 -3
- package/lib/roll-loop-status.py +6 -0
- package/lib/roll_render.py +5 -0
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.524.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **用 pi / deepseek / kimi 跑的 loop 也能在终端里实时看到进度** — 不再黑屏 `[loop]`
|
|
8
|
+
- **`roll slides new` 可以用项目自己的模板** — 不只用内置的了 `[deck]`
|
|
9
|
+
- **`roll slides list` 一眼看出 slide 状态** — 哪些能看、哪些生成失败、失败原因也能查 `[deck]`
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **loop 不再一次吞太多故事** — 每次只做一个,时间可预期 `[loop]`
|
|
14
|
+
- **dashboard 不再对非 Claude 模型显示空白** — 能看出每轮谁跑的 `[loop]`
|
|
15
|
+
|
|
3
16
|
## v2026.524.1
|
|
4
17
|
|
|
5
18
|
### Added
|
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.524.
|
|
7
|
+
VERSION="2026.524.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"
|
|
@@ -3342,11 +3342,13 @@ USAGE 用法
|
|
|
3342
3342
|
roll slides new "<topic>" [--template <name>] [--no-build]
|
|
3343
3343
|
Generate deck.md via AI, then auto-build + open HTML
|
|
3344
3344
|
通过 AI 生成 deck.md,自动渲染并打开 HTML
|
|
3345
|
-
roll slides list List all decks
|
|
3346
|
-
列出 .roll/slides/
|
|
3345
|
+
roll slides list List all decks (built / stale / failed / unbuilt)
|
|
3346
|
+
列出 .roll/slides/ 下所有幻灯片(四态)
|
|
3347
3347
|
roll slides preview <slug> [--no-open]
|
|
3348
3348
|
Open .roll/slides/<slug>.html in the default browser
|
|
3349
3349
|
在浏览器中打开已渲染的幻灯片
|
|
3350
|
+
roll slides logs <slug> Show the last build failure log for a deck
|
|
3351
|
+
显示幻灯片上次构建失败日志
|
|
3350
3352
|
|
|
3351
3353
|
OPTIONS 选项
|
|
3352
3354
|
--no-open Skip auto-opening the rendered HTML in a browser
|
|
@@ -3367,6 +3369,12 @@ _slides_lib() {
|
|
|
3367
3369
|
# Returns 0 + prints the path if the template exists, else returns 1.
|
|
3368
3370
|
_slides_template_path() {
|
|
3369
3371
|
local name="$1"
|
|
3372
|
+
# US-DECK-013: project-level template override takes priority over built-in.
|
|
3373
|
+
local proj_tpl=".roll/slides/templates/${name}.html"
|
|
3374
|
+
if [[ -f "$proj_tpl" ]]; then
|
|
3375
|
+
printf '%s' "$proj_tpl"
|
|
3376
|
+
return 0
|
|
3377
|
+
fi
|
|
3370
3378
|
local tpl="${ROLL_PKG_DIR}/lib/slides/templates/${name}.html"
|
|
3371
3379
|
if [[ -f "$tpl" ]]; then
|
|
3372
3380
|
printf '%s' "$tpl"
|
|
@@ -3459,8 +3467,19 @@ cmd_slides_build() {
|
|
|
3459
3467
|
return 1
|
|
3460
3468
|
fi
|
|
3461
3469
|
|
|
3470
|
+
# US-DECK-011: capture failure to .last-build.err for slides logs
|
|
3471
|
+
local err_file=".roll/slides/${slug}/.last-build.err"
|
|
3472
|
+
|
|
3462
3473
|
# 1. Validate first (fail-fast on AI-generated decks).
|
|
3463
|
-
|
|
3474
|
+
# Exit 2 = grounding warning (schema OK, evidence below threshold) — warn but don't fail.
|
|
3475
|
+
local val_out val_exit
|
|
3476
|
+
val_out=$(python3 "$validator" "$deck" 2>&1) || val_exit=$?
|
|
3477
|
+
if [[ "${val_exit:-0}" -eq 2 ]]; then
|
|
3478
|
+
echo "[roll] ${val_out}" >&2
|
|
3479
|
+
elif [[ "${val_exit:-0}" -ne 0 ]]; then
|
|
3480
|
+
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3481
|
+
mkdir -p ".roll/slides/${slug}"
|
|
3482
|
+
printf '[%s] stage=validate\n%s\n' "$ts" "$val_out" > "$err_file"
|
|
3464
3483
|
err "deck.md validation failed — fix the issues above before building."
|
|
3465
3484
|
echo " deck.md 校验失败,请先修复上方提示再重试。" >&2
|
|
3466
3485
|
return 1
|
|
@@ -3470,16 +3489,25 @@ cmd_slides_build() {
|
|
|
3470
3489
|
local tpl_name; tpl_name=$(_slides_template_for_deck "$deck")
|
|
3471
3490
|
local tpl_path
|
|
3472
3491
|
if ! tpl_path=$(_slides_template_path "$tpl_name"); then
|
|
3492
|
+
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3493
|
+
mkdir -p ".roll/slides/${slug}"
|
|
3494
|
+
printf '[%s] stage=template\ntemplate not found: %s\n' "$ts" "$tpl_name" > "$err_file"
|
|
3473
3495
|
err "Template not found: ${tpl_name} 未找到模板:${tpl_name}"
|
|
3474
3496
|
return 1
|
|
3475
3497
|
fi
|
|
3476
3498
|
|
|
3477
3499
|
local out=".roll/slides/${slug}.html"
|
|
3478
3500
|
mkdir -p ".roll/slides"
|
|
3479
|
-
|
|
3501
|
+
local render_out; render_out=$(python3 "$renderer" "$deck" "$tpl_path" "$out" 2>&1) || {
|
|
3502
|
+
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3503
|
+
mkdir -p ".roll/slides/${slug}"
|
|
3504
|
+
printf '[%s] stage=render\n%s\n' "$ts" "$render_out" > "$err_file"
|
|
3480
3505
|
err "Render failed for ${deck} 渲染失败:${deck}"
|
|
3481
3506
|
return 1
|
|
3482
|
-
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
# US-DECK-011: build succeeded — remove any stale .last-build.err
|
|
3510
|
+
rm -f "$err_file" 2>/dev/null || true
|
|
3483
3511
|
|
|
3484
3512
|
# 3. Default-ignore the HTML artefact so it doesn't accidentally get committed.
|
|
3485
3513
|
_slides_ensure_gitignore
|
|
@@ -3571,31 +3599,42 @@ cmd_slides_list() {
|
|
|
3571
3599
|
IFS=$'\n' sorted_slugs=($(printf '%s\n' "${slugs[@]}" | sort))
|
|
3572
3600
|
unset IFS
|
|
3573
3601
|
|
|
3574
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3602
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3575
3603
|
"slug" "template" "total_slides" "created" "built" "size"
|
|
3576
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3577
|
-
"----" "--------" "------------" "-------" "
|
|
3604
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3605
|
+
"----" "--------" "------------" "-------" "------" "----"
|
|
3578
3606
|
|
|
3579
|
-
local s deck html template total created built size bytes
|
|
3607
|
+
local s deck html err_file template total created built size bytes
|
|
3580
3608
|
for s in "${sorted_slugs[@]}"; do
|
|
3581
3609
|
deck="${slides_dir}/${s}/deck.md"
|
|
3582
3610
|
html="${slides_dir}/${s}.html"
|
|
3611
|
+
err_file="${slides_dir}/${s}/.last-build.err"
|
|
3583
3612
|
template=$(_slides_frontmatter_field "$deck" "template")
|
|
3584
3613
|
[[ -z "$template" ]] && template="-"
|
|
3585
3614
|
total=$(_slides_frontmatter_field "$deck" "total_slides")
|
|
3586
3615
|
[[ -z "$total" ]] && total="-"
|
|
3587
3616
|
created=$(_slides_frontmatter_field "$deck" "created")
|
|
3588
3617
|
[[ -z "$created" ]] && created="-"
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3618
|
+
# US-DECK-011: 4-state built column
|
|
3619
|
+
if [[ -f "$err_file" ]]; then
|
|
3620
|
+
built="⚠ failed"
|
|
3621
|
+
size="-"
|
|
3622
|
+
elif [[ -f "$html" ]]; then
|
|
3623
|
+
# Check if deck.md is newer than the HTML (stale)
|
|
3624
|
+
if [[ "$deck" -nt "$html" ]]; then
|
|
3625
|
+
built="≈ stale"
|
|
3626
|
+
size="-"
|
|
3627
|
+
else
|
|
3628
|
+
built="✓ built"
|
|
3629
|
+
bytes=$(wc -c <"$html" 2>/dev/null | tr -d ' ')
|
|
3630
|
+
[[ -z "$bytes" ]] && bytes=0
|
|
3631
|
+
size=$(_slides_human_size "$bytes")
|
|
3632
|
+
fi
|
|
3594
3633
|
else
|
|
3595
|
-
built="✗"
|
|
3634
|
+
built="✗ unbuilt"
|
|
3596
3635
|
size="-"
|
|
3597
3636
|
fi
|
|
3598
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3637
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3599
3638
|
"$s" "$template" "$total" "$created" "$built" "$size"
|
|
3600
3639
|
done
|
|
3601
3640
|
return 0
|
|
@@ -3645,6 +3684,46 @@ cmd_slides_preview() {
|
|
|
3645
3684
|
return 0
|
|
3646
3685
|
}
|
|
3647
3686
|
|
|
3687
|
+
# ─── US-DECK-011 ─────────────────────────────────────────────────────────────
|
|
3688
|
+
cmd_slides_logs() {
|
|
3689
|
+
local slug=""
|
|
3690
|
+
while [[ $# -gt 0 ]]; do
|
|
3691
|
+
case "$1" in
|
|
3692
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3693
|
+
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3694
|
+
*)
|
|
3695
|
+
if [[ -z "$slug" ]]; then
|
|
3696
|
+
slug="$1"; shift
|
|
3697
|
+
else
|
|
3698
|
+
err "Unexpected argument: $1 多余参数: $1"; return 1
|
|
3699
|
+
fi
|
|
3700
|
+
;;
|
|
3701
|
+
esac
|
|
3702
|
+
done
|
|
3703
|
+
|
|
3704
|
+
if [[ -z "$slug" ]]; then
|
|
3705
|
+
err "Usage: roll slides logs <slug>"
|
|
3706
|
+
echo "用法: roll slides logs <slug>" >&2
|
|
3707
|
+
return 1
|
|
3708
|
+
fi
|
|
3709
|
+
|
|
3710
|
+
local deck_dir=".roll/slides/${slug}"
|
|
3711
|
+
local err_file="${deck_dir}/.last-build.err"
|
|
3712
|
+
|
|
3713
|
+
if [[ ! -d "$deck_dir" ]] || [[ ! -f "${deck_dir}/deck.md" ]]; then
|
|
3714
|
+
err "Deck not found: ${slug} 未找到幻灯片:${slug}"
|
|
3715
|
+
return 1
|
|
3716
|
+
fi
|
|
3717
|
+
|
|
3718
|
+
if [[ ! -f "$err_file" ]]; then
|
|
3719
|
+
info "No failure records for ${slug} 该幻灯片没有失败记录"
|
|
3720
|
+
return 0
|
|
3721
|
+
fi
|
|
3722
|
+
|
|
3723
|
+
cat "$err_file"
|
|
3724
|
+
return 0
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3648
3727
|
# ─── US-DECK-004 ─────────────────────────────────────────────────────────────
|
|
3649
3728
|
# Turn a topic string into a kebab-case slug.
|
|
3650
3729
|
# Lower-cases, replaces any run of non-alphanumerics with a single dash,
|
|
@@ -4001,6 +4080,9 @@ cmd_slides() {
|
|
|
4001
4080
|
preview)
|
|
4002
4081
|
cmd_slides_preview "$@"
|
|
4003
4082
|
;;
|
|
4083
|
+
logs)
|
|
4084
|
+
cmd_slides_logs "$@"
|
|
4085
|
+
;;
|
|
4004
4086
|
--help|-h|help)
|
|
4005
4087
|
_slides_help
|
|
4006
4088
|
return 0
|
|
@@ -5083,6 +5165,7 @@ if _worktree_fetch_origin main \\
|
|
|
5083
5165
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
5084
5166
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
5085
5167
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
5168
|
+
_loop_event agent_used "\${CYCLE_ID}" "\$(_project_agent)" "primary" || true
|
|
5086
5169
|
_phase_end worktree_setup ok
|
|
5087
5170
|
else
|
|
5088
5171
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
@@ -5107,6 +5190,9 @@ FMT="${fmt_script}"
|
|
|
5107
5190
|
export LOOP_PROJECT_SLUG="${slug}"
|
|
5108
5191
|
export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
5109
5192
|
export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
5193
|
+
# US-LOOP-010: tell loop-fmt.py which agent is running so it can branch
|
|
5194
|
+
# rendering: claude → stream-json parser, others → transparent passthrough.
|
|
5195
|
+
export ROLL_LOOP_AGENT="\$(_project_agent)"
|
|
5110
5196
|
_phase_begin claude_invoke
|
|
5111
5197
|
# FIX-115: fallback agent support — when primary fails 3 attempts, try fallback 3 more
|
|
5112
5198
|
FALLBACK_AGENT_NAME="${fallback_agent}"
|
|
@@ -5153,8 +5239,12 @@ done
|
|
|
5153
5239
|
# fallback_agent is configured, try the fallback for 3 more attempts.
|
|
5154
5240
|
if [ "\$_CYCLE_TIMED_OUT" -eq 0 ] && [ "\$_exit" -ne 0 ] && [ -n "\$FALLBACK_AGENT_NAME" ] && [ -n "\$FALLBACK_CMD" ]; then
|
|
5155
5241
|
_loop_event agent_switch "\$FALLBACK_AGENT_NAME" "" "primary failed after 3 attempts" || true
|
|
5242
|
+
_loop_event agent_used "\${CYCLE_ID}" "\$FALLBACK_AGENT_NAME" "fallback" || true
|
|
5156
5243
|
echo "[loop] primary agent failed after 3 attempts — switching to fallback: \$FALLBACK_AGENT_NAME"
|
|
5157
5244
|
_USE_FALLBACK=1
|
|
5245
|
+
# US-LOOP-010: tell loop-fmt which fallback agent is running so it renders
|
|
5246
|
+
# correctly (passthrough for non-claude, stream-json for claude).
|
|
5247
|
+
export ROLL_LOOP_AGENT="\$FALLBACK_AGENT_NAME"
|
|
5158
5248
|
for _attempt in 1 2 3; do
|
|
5159
5249
|
_CYCLE_TIMED_OUT=0
|
|
5160
5250
|
( sleep "\$LOOP_CYCLE_TIMEOUT_SEC" && {
|
|
@@ -5166,7 +5256,11 @@ if [ "\$_CYCLE_TIMED_OUT" -eq 0 ] && [ "\$_exit" -ne 0 ] && [ -n "\$FALLBACK_AGE
|
|
|
5166
5256
|
pkill -KILL -f "\$WT" 2>/dev/null
|
|
5167
5257
|
} ) &
|
|
5168
5258
|
_WATCHDOG_PID=\$!
|
|
5169
|
-
|
|
5259
|
+
if [ -f "\$FMT" ]; then
|
|
5260
|
+
( cd "\$WT" && \$FALLBACK_CMD ) | python3 "\$FMT"
|
|
5261
|
+
else
|
|
5262
|
+
( cd "\$WT" && \$FALLBACK_CMD )
|
|
5263
|
+
fi
|
|
5170
5264
|
_exit=\$?
|
|
5171
5265
|
kill "\$_WATCHDOG_PID" 2>/dev/null
|
|
5172
5266
|
wait "\$_WATCHDOG_PID" 2>/dev/null
|
|
@@ -5637,7 +5731,7 @@ cmd_loop() {
|
|
|
5637
5731
|
on) _loop_on ;;
|
|
5638
5732
|
off) _loop_off ;;
|
|
5639
5733
|
now) _loop_now ;;
|
|
5640
|
-
test) _loop_test ;;
|
|
5734
|
+
test) shift; _loop_test "$@" ;;
|
|
5641
5735
|
status) _loop_status "$@" ;;
|
|
5642
5736
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
5643
5737
|
runs) _loop_runs "$@" ;;
|
|
@@ -5852,17 +5946,36 @@ _loop_test() {
|
|
|
5852
5946
|
local log="${_SHARED_ROOT}/loop/cron-${slug}.log"
|
|
5853
5947
|
local test_runner="${_SHARED_ROOT}/loop/run-${slug}-test.sh"
|
|
5854
5948
|
|
|
5949
|
+
# US-LOOP-010: --agent <name> lets integration tests exercise the
|
|
5950
|
+
# multi-agent passthrough path without needing a real pi binary.
|
|
5951
|
+
local agent="claude"
|
|
5952
|
+
local agent_cmd=""
|
|
5953
|
+
while [[ $# -gt 0 ]]; do
|
|
5954
|
+
case "$1" in
|
|
5955
|
+
--agent) agent="${2:-claude}"; shift 2 ;;
|
|
5956
|
+
--cmd) agent_cmd="${2}"; shift 2 ;;
|
|
5957
|
+
*) shift ;;
|
|
5958
|
+
esac
|
|
5959
|
+
done
|
|
5960
|
+
if [[ -z "$agent_cmd" ]]; then
|
|
5961
|
+
if [[ "$agent" == "claude" ]]; then
|
|
5962
|
+
agent_cmd='claude -p "Reply with a single word: hello"; sleep 10'
|
|
5963
|
+
else
|
|
5964
|
+
agent_cmd="echo 'mock ${agent} output line 1'; echo 'mock ${agent} output line 2'"
|
|
5965
|
+
fi
|
|
5966
|
+
fi
|
|
5967
|
+
|
|
5855
5968
|
# FIX-054: terminal preference removed — runner always uses Terminal.app.
|
|
5856
5969
|
local active_start active_end
|
|
5857
5970
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
5858
5971
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
5859
5972
|
|
|
5860
|
-
info "Generating test runner...
|
|
5973
|
+
info "Generating test runner (agent: ${agent})... 正在生成测试启动脚本 (agent: ${agent})..."
|
|
5861
5974
|
_write_loop_runner_script "$test_runner" "$project_path" \
|
|
5862
|
-
|
|
5975
|
+
"${agent_cmd}" \
|
|
5863
5976
|
"$log" "$active_start" "$active_end"
|
|
5864
5977
|
|
|
5865
|
-
info "Starting smoke test (
|
|
5978
|
+
info "Starting smoke test (agent: ${agent})... 正在运行 smoke 测试 (agent: ${agent})..."
|
|
5866
5979
|
info "Watch for: tmux session + terminal popup + stream-json events flowing"
|
|
5867
5980
|
info "观察:tmux 会话 + 终端弹窗 + stream-json 事件流"
|
|
5868
5981
|
|
|
@@ -5872,9 +5985,9 @@ _loop_test() {
|
|
|
5872
5985
|
local elapsed=$(( $(date +%s) - start_time ))
|
|
5873
5986
|
|
|
5874
5987
|
if [[ $exit_code -eq 0 ]]; then
|
|
5875
|
-
ok "Smoke test passed (${elapsed}s) smoke 测试通过 (${elapsed}
|
|
5988
|
+
ok "Smoke test passed (${elapsed}s, agent: ${agent}) smoke 测试通过 (${elapsed}秒, agent: ${agent})"
|
|
5876
5989
|
else
|
|
5877
|
-
err "Smoke test failed (exit ${exit_code}, ${elapsed}s) smoke 测试失败 (退出码 ${exit_code}, ${elapsed}
|
|
5990
|
+
err "Smoke test failed (exit ${exit_code}, ${elapsed}s, agent: ${agent}) smoke 测试失败 (退出码 ${exit_code}, ${elapsed}s, agent: ${agent})"
|
|
5878
5991
|
return 1
|
|
5879
5992
|
fi
|
|
5880
5993
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/loop-fmt.py
CHANGED
|
@@ -441,11 +441,73 @@ class LoopFmt:
|
|
|
441
441
|
pass # best-effort; never break tmux output
|
|
442
442
|
|
|
443
443
|
|
|
444
|
-
def
|
|
445
|
-
|
|
444
|
+
def _passthrough_main(agent):
|
|
445
|
+
"""Transparent forwarding for non-claude agents (pi, deepseek, kimi, …).
|
|
446
|
+
|
|
447
|
+
Writes every stdin line to stdout with a HH:MM:SS timestamp prefix so
|
|
448
|
+
tmux shows real-time progress. Also appends each line as a lightweight
|
|
449
|
+
'usage'-type event to the per-slug events ndjson — token / cost fields
|
|
450
|
+
are set to null (agent-specific parsing is out of scope for this US).
|
|
451
|
+
"""
|
|
452
|
+
slug = os.environ.get("LOOP_PROJECT_SLUG")
|
|
453
|
+
cycle = os.environ.get("LOOP_CYCLE_ID")
|
|
454
|
+
shared = os.environ.get("LOOP_SHARED_ROOT") or os.path.expanduser("~/.shared/roll")
|
|
455
|
+
evfile = None
|
|
456
|
+
if slug and cycle:
|
|
457
|
+
evfile = os.path.join(shared, "loop", f"events-{slug}.ndjson")
|
|
458
|
+
try:
|
|
459
|
+
os.makedirs(os.path.dirname(evfile), exist_ok=True)
|
|
460
|
+
except Exception:
|
|
461
|
+
evfile = None
|
|
462
|
+
|
|
446
463
|
for line in sys.stdin:
|
|
447
|
-
|
|
464
|
+
if not line.rstrip():
|
|
465
|
+
continue
|
|
466
|
+
# Timestamp prefix so tmux shows activity (even if agent output has
|
|
467
|
+
# no timestamps of its own).
|
|
468
|
+
ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
|
|
469
|
+
out = f"{DARK_GRAY}{ts}{RESET} {line.rstrip()}"
|
|
470
|
+
sys.stdout.write(out + "\n")
|
|
448
471
|
sys.stdout.flush()
|
|
472
|
+
# Emit a lightweight usage event so the cycle has *some* event trace
|
|
473
|
+
# (token/cost are null — parsing those is agent-specific and out of
|
|
474
|
+
# scope for the minimal transparent-passthrough US).
|
|
475
|
+
if evfile:
|
|
476
|
+
_emit_passthrough_event(evfile, cycle, agent, line.rstrip())
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _emit_passthrough_event(evfile, cycle, agent, text):
|
|
480
|
+
"""Best-effort append a usage-type event to evfile."""
|
|
481
|
+
payload = {
|
|
482
|
+
"model": agent,
|
|
483
|
+
"input_tokens": None,
|
|
484
|
+
"output_tokens": None,
|
|
485
|
+
"cost_list_usd": None,
|
|
486
|
+
"duration_ms": None,
|
|
487
|
+
}
|
|
488
|
+
record = json.dumps({
|
|
489
|
+
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
490
|
+
"stage": "usage",
|
|
491
|
+
"label": cycle,
|
|
492
|
+
"detail": payload,
|
|
493
|
+
"outcome": "ok",
|
|
494
|
+
}) + "\n"
|
|
495
|
+
try:
|
|
496
|
+
with open(evfile, "a") as f:
|
|
497
|
+
f.write(record)
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def main():
|
|
503
|
+
agent = os.environ.get("ROLL_LOOP_AGENT", "claude")
|
|
504
|
+
if agent == "claude":
|
|
505
|
+
fmt = LoopFmt()
|
|
506
|
+
for line in sys.stdin:
|
|
507
|
+
fmt.process(line)
|
|
508
|
+
sys.stdout.flush()
|
|
509
|
+
else:
|
|
510
|
+
_passthrough_main(agent)
|
|
449
511
|
|
|
450
512
|
if __name__ == "__main__":
|
|
451
513
|
main()
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -231,6 +231,12 @@ def aggregate(events: List[Dict[str, Any]], cron: List[Dict[str, Any]]) -> List[
|
|
|
231
231
|
sid = _extract_story_id(detail)
|
|
232
232
|
if sid:
|
|
233
233
|
cy["story"] = sid
|
|
234
|
+
elif stage == "agent_used":
|
|
235
|
+
# FIX-119: non-claude agents don't expose model in stream-json.
|
|
236
|
+
# The inner runner emits an agent_used event with the agent name
|
|
237
|
+
# so the dashboard can show it when cy["model"] is None.
|
|
238
|
+
if detail:
|
|
239
|
+
cy["agent"] = detail
|
|
234
240
|
elif stage == "usage":
|
|
235
241
|
# US-LOOP-004: loop-fmt emits this with full token / cost data.
|
|
236
242
|
# Detail is a dict (not the legacy string form).
|
package/lib/roll_render.py
CHANGED
|
@@ -336,6 +336,11 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
336
336
|
sid_c = "red" if outcome == "fail" else "blue"
|
|
337
337
|
|
|
338
338
|
model_label = fmt_model(cy.get("model"))
|
|
339
|
+
# FIX-119: fall back to cy["agent"] (from agent_used event) when model
|
|
340
|
+
# is unknown — non-claude agents (pi, deepseek, kimi) don't expose model
|
|
341
|
+
# info in stream-json, leaving a "—" or "?" on the dashboard.
|
|
342
|
+
if model_label in ("—", "?") and cy.get("agent"):
|
|
343
|
+
model_label = cy["agent"]
|
|
339
344
|
# Auto-hide model column on narrow screens — keeps the dashboard readable
|
|
340
345
|
# when terminal is < 100 cols (cost / story IDs are higher-priority).
|
|
341
346
|
show_model = COLS >= 100
|
package/package.json
CHANGED
|
@@ -75,13 +75,25 @@ denied operations and the cycle will idle-exit.
|
|
|
75
75
|
loop:
|
|
76
76
|
primary_agent: claude # claude | deepseek | kimi
|
|
77
77
|
fallback_agent: deepseek # used when primary fails
|
|
78
|
-
max_items_per_run:
|
|
78
|
+
max_items_per_run: 1 # one story per cycle — atomic delivery, predictable cycle time
|
|
79
79
|
brief_on_feature_complete: true
|
|
80
80
|
retry_backoff: [2, 4, 8, 16] # seconds, exponential
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
## Workflow
|
|
84
84
|
|
|
85
|
+
> **One story per cycle (强约束)**: 每个 cycle 只 pick 一个 Todo、跑完 Step 4
|
|
86
|
+
> 立刻退出,不再回 Step 2 找下一个。理由:
|
|
87
|
+
> - cycle 时间可预测(不会因贪心一连串 PR 撞 45 分钟 hard timeout)
|
|
88
|
+
> - PR / events / dashboard 每行一个故事,归因清晰
|
|
89
|
+
> - 一个故事一个 PR 一次 review,blast radius 最小
|
|
90
|
+
>
|
|
91
|
+
> 唯一例外是依赖修复(CI self-heal 等)已经内嵌在当前故事的 Step 4 里——
|
|
92
|
+
> 那部分不算"新挑故事"。
|
|
93
|
+
>
|
|
94
|
+
> 实现层:max_items_per_run 默认 1,executor skill 跑完 Step 5 必须 exit。
|
|
95
|
+
> 不要在同一个 cycle 内多次 emit `pick_todo` 事件。
|
|
96
|
+
|
|
85
97
|
### Step 1 — Orphan 🔨 Recovery
|
|
86
98
|
|
|
87
99
|
Process-level crash recovery (LOCK, heartbeat, retry budget) is handled by
|
|
@@ -311,6 +323,8 @@ After each item completes:
|
|
|
311
323
|
3. Update state file: `status: idle`
|
|
312
324
|
4. Check if a Feature is now fully complete (all its Stories ✅)
|
|
313
325
|
5. If yes and `brief_on_feature_complete: true` → invoke `Skill("roll-brief")`
|
|
326
|
+
6. **EXIT the cycle.** 不要回 Step 2 找下一个故事,不要再 emit `pick_todo`。
|
|
327
|
+
一个 cycle 只交付一个故事;剩下的 Todo 等下一个 launchd tick 起新 cycle 处理。
|
|
314
328
|
|
|
315
329
|
### Step 5 — Write Run Summary
|
|
316
330
|
|