@seanyao/roll 2026.523.2 → 2026.524.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/roll +411 -75
  3. package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
  4. package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
  5. package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
  6. package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
  8. package/lib/changelog_audit.py +155 -0
  9. package/lib/i18n/agent.sh +0 -0
  10. package/lib/i18n/alert.sh +0 -0
  11. package/lib/i18n/backlog.sh +0 -0
  12. package/lib/i18n/brief.sh +0 -0
  13. package/lib/i18n/changelog.sh +0 -0
  14. package/lib/i18n/ci.sh +0 -0
  15. package/lib/i18n/debug.sh +0 -0
  16. package/lib/i18n/doctor.sh +31 -0
  17. package/lib/i18n/dream.sh +0 -0
  18. package/lib/i18n/init.sh +17 -0
  19. package/lib/i18n/lang.sh +0 -0
  20. package/lib/i18n/loop.sh +0 -0
  21. package/lib/i18n/migrate.sh +0 -0
  22. package/lib/i18n/offboard.sh +15 -0
  23. package/lib/i18n/onboard.sh +0 -0
  24. package/lib/i18n/peer.sh +0 -0
  25. package/lib/i18n/propose.sh +0 -0
  26. package/lib/i18n/release.sh +0 -0
  27. package/lib/i18n/research.sh +0 -0
  28. package/lib/i18n/review_pr.sh +0 -0
  29. package/lib/i18n/sentinel.sh +0 -0
  30. package/lib/i18n/setup.sh +0 -0
  31. package/lib/i18n/shared.sh +83 -0
  32. package/lib/i18n/skills/roll-brief.sh +20 -0
  33. package/lib/i18n/skills/roll-build.sh +97 -0
  34. package/lib/i18n/skills/roll-design.sh +18 -0
  35. package/lib/i18n/skills/roll-fix.sh +14 -0
  36. package/lib/i18n/skills/roll-loop.sh +28 -0
  37. package/lib/i18n/skills/roll-onboard.sh +16 -0
  38. package/lib/i18n/slides.sh +0 -0
  39. package/lib/i18n/status.sh +0 -0
  40. package/lib/i18n/update.sh +9 -0
  41. package/lib/i18n.sh +25 -0
  42. package/lib/loop-fmt.py +12 -8
  43. package/lib/loop_unstick.py +180 -0
  44. package/lib/model_prices.py +93 -12
  45. package/lib/prices/snapshot-2026-05-22.json +2 -0
  46. package/lib/prices/snapshot-2026-05-23-deepseek.json +15 -0
  47. package/lib/prices/snapshot-2026-05-23-kimi.json +14 -0
  48. package/lib/roll-home.py +17 -1
  49. package/lib/roll-loop-status.py +3 -0
  50. package/lib/roll_render.py +5 -2
  51. package/package.json +1 -1
  52. package/skills/roll-.dream/SKILL.md +4 -4
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.523.2"
7
+ VERSION="2026.524.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"
@@ -920,14 +920,14 @@ _doctor_launchd_stale_section() {
920
920
  [[ -d "$wd" ]] && continue
921
921
  if [[ "$found" -eq 0 ]]; then
922
922
  echo ""
923
- echo "Stale launchd plists 无效的 launchd 服务"
923
+ echo "$(msg doctor.stale_plists)"
924
924
  echo ""
925
925
  found=1
926
926
  fi
927
927
  label=$(basename "$plist" .plist)
928
928
  echo " ⚠ ${label}"
929
929
  echo " WorkingDirectory missing: ${wd}"
930
- echo " 路径已失效,可清理: launchctl bootout gui/$(id -u)/${label}; rm '${plist}'"
930
+ echo " $(msg doctor.stale_plists_cleanup): launchctl bootout gui/$(id -u)/${label}; rm '${plist}'"
931
931
  done
932
932
  return 0
933
933
  }
@@ -936,7 +936,8 @@ _doctor_pr_section() {
936
936
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
937
937
 
938
938
  echo ""
939
- echo "PR review extras PR 评审两档开关"
939
+ echo "$(ROLL_LANG_RESOLVED=en msg doctor.pr_review_extras)"
940
+ echo "$(ROLL_LANG_RESOLVED=zh msg doctor.pr_review_extras)"
940
941
  echo ""
941
942
 
942
943
  local protection_state event_state
@@ -945,24 +946,24 @@ _doctor_pr_section() {
945
946
 
946
947
  case "$protection_state" in
947
948
  enabled)
948
- echo " AI review double gate enabled AI 评审双门已启用"
949
+ echo " $(msg doctor.pr_double_gate_enabled)"
949
950
  ;;
950
951
  disabled)
951
- echo " AI review double gate not enabled 双门未启用"
952
+ echo " $(msg doctor.pr_double_gate_disabled)"
952
953
  _print_pr_pipeline_hint
953
954
  ;;
954
955
  *)
955
- echo " AI review double gate state unknown — requires gh auth 状态未知(需要 gh auth)"
956
+ echo " $(msg doctor.pr_double_gate_unknown)"
956
957
  _print_pr_pipeline_hint
957
958
  ;;
958
959
  esac
959
960
 
960
961
  case "$event_state" in
961
962
  present)
962
- echo " Event-driven PR review installed 事件驱动 PR 评审已安装"
963
+ echo " $(msg doctor.pr_event_enabled)"
963
964
  ;;
964
965
  *)
965
- echo " Event-driven PR review not installed 事件驱动 PR 评审未安装"
966
+ echo " $(msg doctor.pr_event_disabled)"
966
967
  _print_pr_event_hint
967
968
  ;;
968
969
  esac
@@ -1000,21 +1001,18 @@ _doctor_event_workflow_state() {
1000
1001
  }
1001
1002
 
1002
1003
  _print_pr_event_hint() {
1003
- cat <<'HINT'
1004
-
1005
- Optional — enable event-driven PR review (seconds-fast, GitHub only).
1006
- 可选 —— 启用事件驱动 PR 评审(秒级响应,仅限 GitHub)。
1007
-
1008
- Without this, Roll reviews PRs each loop cycle (~1h). With it,
1009
- contributors get AI feedback on PR open/update immediately.
1010
- 不安装也行 loop 每轮会兜底评审。安装后 PR 一开即触发 AI 评审。
1011
-
1012
- cp templates/workflows/pr-review-event.yml .github/workflows/
1013
-
1014
- Then set the API key secret for your configured agent in GitHub repo settings.
1015
- 然后在 GitHub 仓库设置中添加你配置的 agent 对应的 API key secret。
1016
-
1017
- HINT
1004
+ echo ""
1005
+ echo " $(msg doctor.pr_event_optional)"
1006
+ echo " $(msg doctor.pr_event_optional_zh)"
1007
+ echo ""
1008
+ echo " $(msg doctor.pr_event_without)"
1009
+ echo " $(msg doctor.pr_event_without_zh)"
1010
+ echo ""
1011
+ echo " cp templates/workflows/pr-review-event.yml .github/workflows/"
1012
+ echo ""
1013
+ echo " $(msg doctor.pr_event_secret)"
1014
+ echo " $(msg doctor.pr_event_secret_zh)"
1015
+ echo ""
1018
1016
  }
1019
1017
 
1020
1018
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -1031,19 +1029,19 @@ _check_installed_version_or_retry() {
1031
1029
  [[ -z "$expected" || -z "$installed" ]] && return 0
1032
1030
 
1033
1031
  if [[ "$installed" != "$expected" ]]; then
1034
- warn "Version mismatch: installed ${installed}, expected ${expected} — CDN propagation lag, clearing cache and retrying... 版本不一致(已安装 ${installed},期望 ${expected}),疑似 CDN 未同步,清理缓存后重试..."
1032
+ warn "$(msg update.version_mismatch "$installed" "$expected")"
1035
1033
  npm cache clean --force &>/dev/null || true
1036
1034
  npm install -g @seanyao/roll@latest &>/dev/null || true
1037
1035
  local after
1038
1036
  after="$(grep "^VERSION=" "${pkg_dir}/@seanyao/roll/bin/roll" 2>/dev/null | sed 's/VERSION="\([^"]*\)"/\1/' || true)"
1039
1037
  if [[ -n "$after" && "$after" != "$expected" ]]; then
1040
- warn "Still on ${after} after retry — registry may not have propagated yet, try again in a minute. 重试后仍为 ${after},注册表可能尚未同步,请稍后再试。"
1038
+ warn "$(msg update.still_mismatch "$after")"
1041
1039
  fi
1042
1040
  fi
1043
1041
  }
1044
1042
 
1045
1043
  cmd_update() {
1046
- info "Current version: roll v${VERSION} 当前版本: roll v${VERSION}"
1044
+ info "$(msg update.current_version "$VERSION")"
1047
1045
  info "Upgrading via npm... 正在通过 npm 升级..."
1048
1046
  echo ""
1049
1047
 
@@ -1520,16 +1518,16 @@ _onboard_failure_hint() {
1520
1518
  local agent="$1" code="$2"
1521
1519
  echo "" >&2
1522
1520
  if [[ "$code" == "130" ]]; then
1523
- err "Onboard cancelled (Ctrl-C). Onboard 已取消(Ctrl-C)。" >&2
1521
+ err "$(msg init.onboard_cancelled)" >&2
1524
1522
  else
1525
- err "Agent '${agent}' exited with code ${code}. agent '${agent}' 异常退出 (code ${code})" >&2
1523
+ err "$(msg init.onboard_agent_exited "$agent" "$code")" >&2
1526
1524
  fi
1527
1525
  echo "" >&2
1528
- echo " 下一步 Next step:" >&2
1529
- echo " - 再试一次同一个 agent: roll init" >&2
1530
- echo " - retry the same agent: roll init" >&2
1531
- echo " - 换一个 agent: ROLL_ONBOARD_AGENT=<name> roll init" >&2
1532
- echo " - switch to another: ROLL_ONBOARD_AGENT=<name> roll init" >&2
1526
+ echo " $(msg init.onboard_next_step)" >&2
1527
+ echo " - $(msg init.onboard_retry)" >&2
1528
+ echo " - $(msg init.onboard_retry_en)" >&2
1529
+ echo " - $(msg init.onboard_switch)" >&2
1530
+ echo " - $(msg init.onboard_switch_en)" >&2
1533
1531
  echo "" >&2
1534
1532
  }
1535
1533
 
@@ -1821,13 +1819,13 @@ cmd_offboard() {
1821
1819
  local changeset; changeset=$(_onboard_changeset_path "$project_dir")
1822
1820
 
1823
1821
  if [[ ! -f "$changeset" ]]; then
1824
- err "No onboard changeset found at .roll/onboard-changeset.yaml"
1825
- err "未找到 onboard 变更清单 .roll/onboard-changeset.yaml"
1822
+ err "$(msg offboard.no_changeset_en)"
1823
+ err "$(msg offboard.no_changeset_zh)"
1826
1824
  echo "" >&2
1827
- echo " Manual offboard — remove these by hand if they came from Roll:" >&2
1828
- echo " rm -rf .roll/ # all process artefacts" >&2
1829
- echo " rm -f AGENTS.md CLAUDE.md # only if they were generated by roll init" >&2
1830
- echo " Edit .gitignore to remove any '.roll/' entry" >&2
1825
+ echo " $(msg offboard.manual_offboard)" >&2
1826
+ echo " $(msg offboard.manual_rm_roll)" >&2
1827
+ echo " $(msg offboard.manual_rm_agents)" >&2
1828
+ echo " $(msg offboard.manual_edit_gitignore)" >&2
1831
1829
  return 1
1832
1830
  fi
1833
1831
 
@@ -3086,6 +3084,18 @@ _project_agent() {
3086
3084
  fi
3087
3085
  }
3088
3086
 
3087
+ _fallback_agent() {
3088
+ local pref new_pref
3089
+ new_pref=$(_project_agent_pref_file)
3090
+ if [[ -f "$new_pref" ]] && grep -q "^fallback_agent:" "$new_pref" 2>/dev/null; then
3091
+ grep "^fallback_agent:" "$new_pref" | awk '{print $2}' | tr -d '"' | head -1
3092
+ elif [[ -f ".roll.yaml" ]] && grep -q "^fallback_agent:" .roll.yaml 2>/dev/null; then
3093
+ grep "^fallback_agent:" .roll.yaml | awk '{print $2}' | tr -d '"' | head -1
3094
+ elif [[ -f "$ROLL_CONFIG" ]] && grep -q "fallback_agent:" "$ROLL_CONFIG" 2>/dev/null; then
3095
+ grep "fallback_agent:" "$ROLL_CONFIG" | awk '{print $2}' | tr -d '"' | head -1
3096
+ fi
3097
+ }
3098
+
3089
3099
  _skill_content() {
3090
3100
  # Strip YAML frontmatter (---...---) — it's roll-internal metadata, not agent instructions
3091
3101
  awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' "$1"
@@ -3211,6 +3221,115 @@ _agent_run_skill() {
3211
3221
  # `list` / `preview` (DECK-005). The AI authoring step happens in `new`;
3212
3222
  # everything else is pure bash.
3213
3223
  # ═══════════════════════════════════════════════════════════════════════════════
3224
+ # ─── Progress helpers (US-DECK-010) ──────────────────────────────────────────
3225
+ # Shared state for slides progress reporting.
3226
+
3227
+ _SLIDES_PROGRESS_PHASES=()
3228
+ _SLIDES_PROGRESS_START_TIME=0
3229
+ _SLIDES_PROGRESS_CURRENT=""
3230
+ _SLIDES_PROGRESS_PHASE_START_TIME=0
3231
+ _SLIDES_PROGRESS_SPINNER_PID=""
3232
+ _SLIDES_PROGRESS_QUIET=0
3233
+
3234
+ _slides_progress_init() {
3235
+ _SLIDES_PROGRESS_PHASES=("$@")
3236
+ _SLIDES_PROGRESS_START_TIME=$(date +%s)
3237
+ _SLIDES_PROGRESS_CURRENT=""
3238
+ _SLIDES_PROGRESS_PHASE_START_TIME=0
3239
+ _SLIDES_PROGRESS_SPINNER_PID=""
3240
+ _SLIDES_PROGRESS_QUIET=0
3241
+ }
3242
+
3243
+ _slides_progress_quiet() {
3244
+ _SLIDES_PROGRESS_QUIET=1
3245
+ }
3246
+
3247
+ _slides_progress_elapsed_str() {
3248
+ local elapsed="${1:-0}"
3249
+ local min=$(( elapsed / 60 ))
3250
+ local sec=$(( elapsed % 60 ))
3251
+ if [[ $min -gt 0 ]]; then
3252
+ printf '%dm %ds' "$min" "$sec"
3253
+ else
3254
+ printf '%ds' "$sec"
3255
+ fi
3256
+ }
3257
+
3258
+ _slides_progress_detect_tty() {
3259
+ if [[ ! -t 1 ]]; then
3260
+ _slides_progress_quiet
3261
+ fi
3262
+ }
3263
+
3264
+ _slides_progress_phase_enter() {
3265
+ local phase="$1"
3266
+ local now
3267
+ now=$(date +%s)
3268
+
3269
+ # Stop any running spinner
3270
+ if [[ -n "${_SLIDES_PROGRESS_SPINNER_PID:-}" ]]; then
3271
+ kill "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
3272
+ wait "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
3273
+ _SLIDES_PROGRESS_SPINNER_PID=""
3274
+ if [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]]; then
3275
+ printf '\r\033[K' >&2
3276
+ fi
3277
+ fi
3278
+
3279
+ # Print previous phase completion
3280
+ if [[ -n "$_SLIDES_PROGRESS_CURRENT" && $_SLIDES_PROGRESS_PHASE_START_TIME -gt 0 ]]; then
3281
+ local prev_elapsed=$(( now - _SLIDES_PROGRESS_PHASE_START_TIME ))
3282
+ local elapsed_str
3283
+ elapsed_str=$(_slides_progress_elapsed_str "$prev_elapsed")
3284
+ if [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]]; then
3285
+ printf ' ✅ %s (%s)\n' "$_SLIDES_PROGRESS_CURRENT" "$elapsed_str" >&2
3286
+ fi
3287
+ fi
3288
+
3289
+ _SLIDES_PROGRESS_CURRENT="$phase"
3290
+ _SLIDES_PROGRESS_PHASE_START_TIME="$now"
3291
+
3292
+ if [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]]; then
3293
+ printf '→ %s ...\n' "$phase" >&2
3294
+ fi
3295
+ return 0
3296
+ }
3297
+
3298
+ _slides_progress_spinner_start() {
3299
+ [[ $_SLIDES_PROGRESS_QUIET -eq 1 ]] && return 0
3300
+
3301
+ local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
3302
+ local delay=0.1
3303
+ local label_file="${TEST_TMP:-/tmp}/roll-slides-spinner-label-$$"
3304
+ printf '%s' "$_SLIDES_PROGRESS_CURRENT" > "$label_file"
3305
+ (
3306
+ local i=0
3307
+ while true; do
3308
+ local label
3309
+ label=$(cat "$label_file" 2>/dev/null || echo "$_SLIDES_PROGRESS_CURRENT")
3310
+ printf '\r\033[K%s %s' "${chars:$i:1}" "$label" >&2
3311
+ i=$(( (i+1) % ${#chars} ))
3312
+ sleep "$delay"
3313
+ done
3314
+ ) &
3315
+ _SLIDES_PROGRESS_SPINNER_PID=$!
3316
+ }
3317
+
3318
+ _slides_progress_spinner_label() {
3319
+ local label="$1"
3320
+ local label_file="${TEST_TMP:-/tmp}/roll-slides-spinner-label-$$"
3321
+ printf '%s' "$label" > "$label_file"
3322
+ }
3323
+
3324
+ _slides_progress_ok() {
3325
+ local phase="$1"
3326
+ local elapsed="${2:-0}"
3327
+ local elapsed_str
3328
+ elapsed_str=$(_slides_progress_elapsed_str "$elapsed")
3329
+ [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]] && printf ' ✅ %s (%s)\n' "$phase" "$elapsed_str" >&2
3330
+ }
3331
+
3332
+ # ─── slides help ─────────────────────────────────────────────────────────────
3214
3333
  _slides_help() {
3215
3334
  cat <<'EOF'
3216
3335
  roll slides — deck.md → HTML rendering
@@ -3220,9 +3339,9 @@ USAGE 用法
3220
3339
  roll slides build <slug> [--no-open]
3221
3340
  Render .roll/slides/<slug>/deck.md → .roll/slides/<slug>.html
3222
3341
  渲染 deck.md 为 HTML 并自动打开浏览器
3223
- roll slides new "<topic>" [--template <name>]
3224
- Generate a new deck.md from a topic via the selected AI agent
3225
- 通过所选 AI agent 根据主题生成新的 deck.md
3342
+ roll slides new "<topic>" [--template <name>] [--no-build]
3343
+ Generate deck.md via AI, then auto-build + open HTML
3344
+ 通过 AI 生成 deck.md,自动渲染并打开 HTML
3226
3345
  roll slides list List all decks under .roll/slides/ as a table
3227
3346
  列出 .roll/slides/ 下所有幻灯片
3228
3347
  roll slides preview <slug> [--no-open]
@@ -3232,6 +3351,8 @@ USAGE 用法
3232
3351
  OPTIONS 选项
3233
3352
  --no-open Skip auto-opening the rendered HTML in a browser
3234
3353
  渲染后不自动打开浏览器
3354
+ --no-build Skip auto-build after agent completes (deck.md only)
3355
+ 仅生成 deck.md,不自动渲染
3235
3356
  --help, -h Show this help
3236
3357
  显示本帮助
3237
3358
  EOF
@@ -3546,13 +3667,15 @@ _slides_topic_slug() {
3546
3667
  # `.roll/slides/<slug>/deck.md` (and nothing else). After the agent exits,
3547
3668
  # we print a bilingual hint pointing at `roll slides build <slug>`.
3548
3669
  cmd_slides_new() {
3549
- local topic="" template="introduction-v3"
3670
+ local topic="" template="introduction-v3" quiet=0 no_build=0
3550
3671
  while [[ $# -gt 0 ]]; do
3551
3672
  case "$1" in
3552
3673
  --template)
3553
3674
  [[ -n "${2:-}" ]] || { err "--template requires a value --template 需要一个值"; return 1; }
3554
3675
  template="$2"; shift 2 ;;
3555
3676
  --template=*) template="${1#--template=}"; shift ;;
3677
+ --quiet) quiet=1; shift ;;
3678
+ --no-build) no_build=1; shift ;;
3556
3679
  --help|-h) _slides_help; return 0 ;;
3557
3680
  --*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
3558
3681
  *)
@@ -3566,8 +3689,8 @@ cmd_slides_new() {
3566
3689
  done
3567
3690
 
3568
3691
  if [[ -z "$topic" ]]; then
3569
- err "Usage: roll slides new \"<topic>\" [--template <name>]"
3570
- echo " 用法:roll slides new \"<主题>\" [--template <模板名>]" >&2
3692
+ err "Usage: roll slides new \"<topic>\" [--template <name>] [--quiet] [--no-build]"
3693
+ echo " 用法:roll slides new \"<主题>\" [--template <模板名>] [--quiet] [--no-build]" >&2
3571
3694
  return 1
3572
3695
  fi
3573
3696
 
@@ -3578,14 +3701,26 @@ cmd_slides_new() {
3578
3701
  return 1
3579
3702
  fi
3580
3703
 
3704
+ # Progress: init with 5 phases, detect quiet mode
3705
+ _slides_progress_init launching generating validating rendering opening
3706
+ _slides_progress_detect_tty
3707
+ if [[ "$quiet" -eq 1 ]]; then
3708
+ _slides_progress_quiet
3709
+ fi
3710
+
3711
+ # Ctrl-C trap: stop spinner and clean cursor
3712
+ trap '_slides_progress_phase_enter "cancelled" 2>/dev/null; exit 130' INT TERM
3713
+
3714
+ # Phase: launching
3715
+ _slides_progress_phase_enter "launching"
3716
+
3581
3717
  local skill_file="${ROLL_PKG_DIR}/skills/roll-deck/SKILL.md"
3582
3718
  [[ -f "$skill_file" ]] || { err "Skill not found: ${skill_file}"; return 1; }
3583
3719
  local skill_body; skill_body=$(_skill_content "$skill_file")
3584
3720
 
3585
3721
  local agent; agent=$(_project_agent)
3586
3722
 
3587
- # Compose the full prompt: skill body + concrete task context. The agent
3588
- # reads the skill, then sees the topic / slug / template it must use.
3723
+ # Compose the full prompt
3589
3724
  local prompt
3590
3725
  prompt="$(cat <<EOF
3591
3726
  ${skill_body}
@@ -3607,16 +3742,81 @@ EOF
3607
3742
 
3608
3743
  _agent_argv "$agent" text "$prompt" || {
3609
3744
  err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
3745
+ trap - INT TERM
3610
3746
  return 1
3611
3747
  }
3612
3748
 
3613
- info "Launching ${agent} with roll-deck skill for topic: ${topic}"
3614
- info "启动 ${agent} 处理主题:${topic}"
3615
- "${_AGENT_ARGV[@]}"
3749
+ # Phase: generating — launch agent in background + spinner + file-watch
3750
+ _slides_progress_phase_enter "generating"
3751
+
3752
+ local deck_file=".roll/slides/${slug}/deck.md"
3753
+ local deck_dir; deck_dir="$(dirname "$deck_file")"
3754
+ mkdir -p "$deck_dir"
3755
+
3756
+ # Start spinner
3757
+ _slides_progress_spinner_start
3758
+
3759
+ # Launch agent in background
3760
+ "${_AGENT_ARGV[@]}" &
3761
+ local agent_pid=$!
3762
+
3763
+ # File-watch: poll deck.md for ## Slide N count and update spinner label
3764
+ local slide_count=0 last_slide_count=-1
3765
+ while kill -0 "$agent_pid" 2>/dev/null; do
3766
+ if [[ -f "$deck_file" ]]; then
3767
+ slide_count=$(grep -c '^## Slide ' "$deck_file" 2>/dev/null || echo 0)
3768
+ if [[ "$slide_count" != "$last_slide_count" ]]; then
3769
+ last_slide_count="$slide_count"
3770
+ if [[ "$slide_count" -gt 0 ]]; then
3771
+ _slides_progress_spinner_label "generating slide ${slide_count}/18"
3772
+ fi
3773
+ fi
3774
+ fi
3775
+ sleep 1
3776
+ done
3777
+
3778
+ # Agent finished — stop spinner
3779
+ if [[ -n "${_SLIDES_PROGRESS_SPINNER_PID:-}" ]]; then
3780
+ kill "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
3781
+ wait "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
3782
+ _SLIDES_PROGRESS_SPINNER_PID=""
3783
+ [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]] && printf '\r\033[K' >&2
3784
+ fi
3785
+
3786
+ wait "$agent_pid"
3616
3787
  local rc=$?
3617
3788
 
3618
- # Whether the agent succeeded or not, point the user at the next bash step.
3619
- # (Build will surface its own validation errors if deck.md is malformed.)
3789
+ # Phase: validating
3790
+ _slides_progress_phase_enter "validating"
3791
+
3792
+ # If deck.md exists, validate it
3793
+ if [[ -f "$deck_file" ]]; then
3794
+ local lib_dir; lib_dir=$(_slides_lib)
3795
+ if python3 "${lib_dir}/slides-validate.py" "$deck_file" >/dev/null 2>&1; then
3796
+ : # validation passed
3797
+ else
3798
+ [[ $_SLIDES_PROGRESS_QUIET -eq 0 ]] && printf ' ⚠️ validation warnings (build may surface details)\n' >&2
3799
+ fi
3800
+ fi
3801
+
3802
+ # Phase: rendering + opening (unless --no-build)
3803
+ if [[ "$no_build" -eq 0 ]] && [[ -f "$deck_file" ]]; then
3804
+ _slides_progress_phase_enter "rendering"
3805
+ cmd_slides_build "$slug" --no-open
3806
+ local build_rc=$?
3807
+
3808
+ if [[ $build_rc -eq 0 ]]; then
3809
+ _slides_progress_phase_enter "opening"
3810
+ local opener
3811
+ if opener=$(_slides_open_cmd); then
3812
+ "$opener" ".roll/slides/${slug}.html" >/dev/null 2>&1 || true
3813
+ fi
3814
+ fi
3815
+ fi
3816
+
3817
+ trap - INT TERM
3818
+
3819
+ # Print Next hint (unless --quiet flag was explicitly passed)
3620
3820
  echo
3621
3821
  echo "Next: roll slides build ${slug}"
3622
3822
  echo "下一步:roll slides build ${slug}"
@@ -3624,14 +3824,15 @@ EOF
3624
3824
  return "$rc"
3625
3825
  }
3626
3826
 
3627
- # ─── cmd_prices (US-VIEW-013) ─────────────────────────────────────────────────
3827
+ # ─── cmd_prices (US-VIEW-013 / FIX-116) ───────────────────────────────────────
3628
3828
  # `roll prices show` — print the current price snapshot table.
3629
3829
  # `roll prices refresh` — fetch live pricing docs, diff vs latest snapshot,
3630
3830
  # write a new snapshot when rates have changed.
3831
+ # FIX-116: --vendor flag for multi-vendor support.
3631
3832
  _prices_help() {
3632
3833
  cat <<'EOF'
3633
- Usage: roll prices <subcommand> [--url URL]
3634
- roll prices <子命令> [--url 网址]
3834
+ Usage: roll prices <subcommand> [--url URL] [--vendor VENDOR]
3835
+ roll prices <子命令> [--url 网址] [--vendor 厂商]
3635
3836
 
3636
3837
  Subcommands:
3637
3838
  show Print the current price snapshot table.
@@ -3639,6 +3840,10 @@ Subcommands:
3639
3840
  refresh Fetch the official pricing docs, diff against the latest snapshot,
3640
3841
  and write a new snapshot only when rates have changed.
3641
3842
  拉取官方价格文档与最新快照对比,有变化才落新快照。
3843
+
3844
+ Options:
3845
+ --vendor anthropic|deepseek|kimi Target vendor for refresh (default: anthropic).
3846
+ 指定拉取价格的厂商(默认:anthropic)。
3642
3847
  EOF
3643
3848
  }
3644
3849
 
@@ -3654,36 +3859,60 @@ version, effective_at, source_url = mp.snapshot_meta()
3654
3859
  print(f"price snapshot 价格快照")
3655
3860
  print(f" version {version}")
3656
3861
  print(f" effective_at {effective_at}")
3657
- print(f" source {source_url}")
3658
- print(f" default_model {mp.DEFAULT}")
3862
+ print(f" snapshots {len(mp._SNAPSHOTS)} loaded 已加载")
3863
+ for snap in mp._SNAPSHOTS:
3864
+ v = snap.get("vendor", "—")
3865
+ c = snap.get("currency", "—")
3866
+ print(f" {v:<12} {c:>4} {snap['source_url']}")
3659
3867
  print()
3660
- print(f" {'model':<22}{'in':>8}{'out':>8}{'cw':>8}{'cr':>8}")
3868
+ print(f" {'model':<24}{'cur':>4}{'in':>10}{'out':>10}{'cw':>10}{'cr':>10}")
3661
3869
  for model in sorted(mp.PRICES):
3662
3870
  p = mp.PRICES[model]
3663
- print(f" {model:<22}{p['in']:>8.2f}{p['out']:>8.2f}{p['cache_create']:>8.2f}{p['cache_read']:>8.2f}")
3871
+ cur = mp.currency_for(model)
3872
+ print(f" {model:<24}{cur:>4}{p['in']:>10.4f}{p['out']:>10.4f}{p['cache_create']:>10.4f}{p['cache_read']:>10.4f}")
3664
3873
  print()
3665
- print("rates per million tokens, USD 单位为每百万 token 美元")
3874
+ print("rates per million tokens 每百万 token 单价")
3666
3875
  PY
3667
3876
  }
3668
3877
 
3669
3878
  cmd_prices_refresh() {
3670
3879
  local url=""
3880
+ local vendor=""
3671
3881
  while (( $# > 0 )); do
3672
3882
  case "$1" in
3673
3883
  --url) url="$2"; shift 2 ;;
3884
+ --vendor) vendor="$2"; shift 2 ;;
3674
3885
  *) err "Unknown flag: $1 未知参数: $1"; return 1 ;;
3675
3886
  esac
3676
3887
  done
3677
3888
  local lib_dir="${ROLL_PKG_DIR}/lib"
3678
- python3 - "$lib_dir" "$url" <<'PY'
3889
+ python3 - "$lib_dir" "$url" "$vendor" <<'PY'
3679
3890
  import os, sys
3680
3891
  lib_dir = sys.argv[1]
3681
3892
  override_url = sys.argv[2] if len(sys.argv) > 2 else ""
3893
+ vendor = sys.argv[3] if len(sys.argv) > 3 else ""
3682
3894
  sys.path.insert(0, lib_dir)
3683
3895
  import prices_fetcher as pf
3684
3896
 
3685
3897
  snapshot_dir = os.path.join(lib_dir, "prices")
3686
- url = override_url or pf.DEFAULT_SOURCE_URL
3898
+
3899
+ # FIX-116: vendor-specific default URLs.
3900
+ VENDOR_URLS = {
3901
+ "anthropic": "https://platform.claude.com/docs/en/about-claude/pricing",
3902
+ "deepseek": "https://api-docs.deepseek.com/quick_start/pricing",
3903
+ "kimi": "https://platform.kimi.com/docs/pricing/chat",
3904
+ }
3905
+
3906
+ if vendor:
3907
+ default_url = VENDOR_URLS.get(vendor)
3908
+ if not default_url:
3909
+ print(f"[roll] unknown vendor: {vendor} 未知厂商", file=sys.stderr)
3910
+ print(f"[roll] known vendors: {', '.join(sorted(VENDOR_URLS))} 已知厂商", file=sys.stderr)
3911
+ sys.exit(1)
3912
+ url = override_url or default_url
3913
+ pf.DEFAULT_SOURCE_URL = url
3914
+ else:
3915
+ url = override_url or pf.DEFAULT_SOURCE_URL
3687
3916
 
3688
3917
  try:
3689
3918
  action, changes = pf.refresh(snapshot_dir=snapshot_dir, url=url)
@@ -3725,6 +3954,37 @@ cmd_prices() {
3725
3954
  esac
3726
3955
  }
3727
3956
 
3957
+ # FIX-113: changelog audit — list PRs merged to main since latest release
3958
+ # tag that don't appear in CHANGELOG.md's ## Unreleased section.
3959
+ cmd_changelog() {
3960
+ local subcmd="${1:-audit}"
3961
+ shift || true
3962
+ case "$subcmd" in
3963
+ audit)
3964
+ python3 "${ROLL_PKG_DIR}/lib/changelog_audit.py" "$@"
3965
+ ;;
3966
+ --help|-h|help)
3967
+ cat <<EOF
3968
+ Usage: roll changelog audit [--since <tag>] [--json]
3969
+ 发版前体检
3970
+ Scan PRs merged to main since latest v* tag; flag ones that look user-visible
3971
+ (carry US-/FIX-/REFACTOR- id) but are missing from CHANGELOG.md '## Unreleased'.
3972
+
3973
+ Read-only — never edits CHANGELOG. Use to catch drift before release.
3974
+
3975
+ roll changelog audit # check against latest v* tag
3976
+ roll changelog audit --since v2026.520.1
3977
+ roll changelog audit --json # machine-readable
3978
+ EOF
3979
+ ;;
3980
+ *)
3981
+ err "Unknown subcommand: ${subcmd} 未知子命令:${subcmd}"
3982
+ err "Try: roll changelog audit"
3983
+ return 1
3984
+ ;;
3985
+ esac
3986
+ }
3987
+
3728
3988
  cmd_slides() {
3729
3989
  local subcmd="${1:-}"
3730
3990
  shift || true
@@ -4428,6 +4688,21 @@ _write_loop_runner_script() {
4428
4688
  claude_cmd="${claude_cmd/--output-format stream-json/--output-format stream-json --add-dir \"\$WT\"}"
4429
4689
  fi
4430
4690
  local slug; slug=$(_project_slug "$project_path")
4691
+ # FIX-115: build fallback agent command. When primary agent fails all 3
4692
+ # attempts, the inner runner switches to this command for 3 more attempts.
4693
+ local fallback_agent; fallback_agent=$(_fallback_agent)
4694
+ local fallback_cmd=""
4695
+ if [[ -n "$fallback_agent" ]]; then
4696
+ local fallback_skill_file="${ROLL_HOME}/skills/roll-loop/SKILL.md"
4697
+ if [[ -f "$fallback_skill_file" ]]; then
4698
+ local fallback_prompt; fallback_prompt=$(_skill_content "$fallback_skill_file")
4699
+ if _agent_argv "$fallback_agent" plain "$fallback_prompt" 2>/dev/null; then
4700
+ _AGENT_ARGV[0]=$(command -v "${_AGENT_ARGV[0]}" 2>/dev/null || echo "${_AGENT_ARGV[0]}")
4701
+ printf -v fallback_cmd '%q ' "${_AGENT_ARGV[@]}"
4702
+ fallback_cmd="${fallback_cmd% }"
4703
+ fi
4704
+ fi
4705
+ fi
4431
4706
  cat > "$inner_path" << INNER
4432
4707
  #!/bin/bash -l
4433
4708
  set -o pipefail
@@ -4755,6 +5030,18 @@ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
4755
5030
  [ -d "\${_orphan_wt}/.git" ] || [ -f "\${_orphan_wt}/.git" ] || continue
4756
5031
  _orphan_branch=\$(cd "\$_orphan_wt" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
4757
5032
  [ -z "\$_orphan_branch" ] && continue
5033
+ # FIX-114: PR for this branch may already be squash-merged externally. In
5034
+ # that case origin/main..HEAD still shows commits (squash created new SHA)
5035
+ # so the old "needs republish" path tries to recreate the PR and fails.
5036
+ # Ask gh first; if MERGED → drop the worktree clean.
5037
+ if command -v gh >/dev/null 2>&1; then
5038
+ _orphan_pr_state=\$(gh pr view "\$_orphan_branch" --json state -q .state 2>/dev/null || echo "")
5039
+ if [ "\$_orphan_pr_state" = "MERGED" ]; then
5040
+ echo "[loop] FIX-114: orphan worktree \$_orphan_wt branch \$_orphan_branch already merged remotely; cleaning up"
5041
+ _worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
5042
+ continue
5043
+ fi
5044
+ fi
4758
5045
  _orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
4759
5046
  if [ "\$_orphan_commits" -gt 0 ]; then
4760
5047
  echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
@@ -4821,6 +5108,10 @@ export LOOP_PROJECT_SLUG="${slug}"
4821
5108
  export LOOP_CYCLE_ID="\${CYCLE_ID}"
4822
5109
  export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
4823
5110
  _phase_begin claude_invoke
5111
+ # FIX-115: fallback agent support — when primary fails 3 attempts, try fallback 3 more
5112
+ FALLBACK_AGENT_NAME="${fallback_agent}"
5113
+ FALLBACK_CMD="${fallback_cmd}"
5114
+ _USE_FALLBACK=0
4824
5115
  for _attempt in 1 2 3; do
4825
5116
  # FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
4826
5117
  # the SIGTERM result of the previous attempt and would otherwise force an
@@ -4858,6 +5149,36 @@ for _attempt in 1 2 3; do
4858
5149
  fi
4859
5150
  done
4860
5151
 
5152
+ # FIX-115: fallback agent retry — when primary fails all 3 attempts and a
5153
+ # fallback_agent is configured, try the fallback for 3 more attempts.
5154
+ if [ "\$_CYCLE_TIMED_OUT" -eq 0 ] && [ "\$_exit" -ne 0 ] && [ -n "\$FALLBACK_AGENT_NAME" ] && [ -n "\$FALLBACK_CMD" ]; then
5155
+ _loop_event agent_switch "\$FALLBACK_AGENT_NAME" "" "primary failed after 3 attempts" || true
5156
+ echo "[loop] primary agent failed after 3 attempts — switching to fallback: \$FALLBACK_AGENT_NAME"
5157
+ _USE_FALLBACK=1
5158
+ for _attempt in 1 2 3; do
5159
+ _CYCLE_TIMED_OUT=0
5160
+ ( sleep "\$LOOP_CYCLE_TIMEOUT_SEC" && {
5161
+ kill -TERM \$\$ 2>/dev/null
5162
+ pkill -TERM -P \$\$ 2>/dev/null
5163
+ pkill -TERM -f "\$WT" 2>/dev/null
5164
+ sleep 5
5165
+ pkill -KILL -P \$\$ 2>/dev/null
5166
+ pkill -KILL -f "\$WT" 2>/dev/null
5167
+ } ) &
5168
+ _WATCHDOG_PID=\$!
5169
+ ( cd "\$WT" && \$FALLBACK_CMD )
5170
+ _exit=\$?
5171
+ kill "\$_WATCHDOG_PID" 2>/dev/null
5172
+ wait "\$_WATCHDOG_PID" 2>/dev/null
5173
+ [ "\$_CYCLE_TIMED_OUT" -eq 1 ] && break
5174
+ [ "\$_exit" -eq 0 ] && break
5175
+ if [ "\$_attempt" -lt 3 ]; then
5176
+ echo "[loop] \$FALLBACK_AGENT_NAME exited \$_exit (attempt \$_attempt/3) — retrying in 30s..."
5177
+ sleep 30
5178
+ fi
5179
+ done
5180
+ fi
5181
+
4861
5182
  if [ "\$_CYCLE_TIMED_OUT" -eq 1 ] || [ "\$_exit" -ne 0 ]; then
4862
5183
  _phase_end claude_invoke fail
4863
5184
  else
@@ -6296,8 +6617,13 @@ _loop_check_depends_on() {
6296
6617
  }
6297
6618
 
6298
6619
  # _loop_is_manual_only <story-id> [backlog-path]
6299
- # Exit 0: story's own row carries `manual-only:true`.
6620
+ # Exit 0: story's own row carries any manual-only:<value> tag.
6300
6621
  # Exit 1: tag absent, story-id not found, or backlog missing.
6622
+ #
6623
+ # FIX-109: loop used to accept only the literal `manual-only:true` value;
6624
+ # anything else (e.g. `manual-only:roll-meta`, `manual-only:sean-yao`) was
6625
+ # ignored and the story got picked. Now any non-empty `manual-only:<value>`
6626
+ # qualifies — lets users tag rows reserved for a specific human or scope.
6301
6627
  _loop_is_manual_only() {
6302
6628
  local id="$1"
6303
6629
  local backlog="${2:-.roll/backlog.md}"
@@ -6308,7 +6634,7 @@ _loop_is_manual_only() {
6308
6634
  row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
6309
6635
  [ -n "$row" ] || return 1
6310
6636
 
6311
- echo "$row" | grep -qE 'manual-only:true'
6637
+ echo "$row" | grep -qE 'manual-only:[A-Za-z0-9_.-]+'
6312
6638
  }
6313
6639
 
6314
6640
  # US-AUTO-034: PR-first inbox — loop processes open PRs before scanning BACKLOG.
@@ -7865,6 +8191,14 @@ cmd_backlog() {
7865
8191
  _backlog_lint "$@" "$backlog"
7866
8192
  return
7867
8193
  ;;
8194
+ unstick)
8195
+ # FIX-112: revert 🔨 In Progress stories whose latest cycle ended
8196
+ # failed / aborted / blocked > N hours ago (default 4). Conservative
8197
+ # gate so it never undoes legitimately-in-progress work.
8198
+ shift
8199
+ python3 "${ROLL_PKG_DIR}/lib/loop_unstick.py" "$@"
8200
+ return
8201
+ ;;
7868
8202
  block|defer|unblock|promote)
7869
8203
  local pattern="${2:-}"
7870
8204
  local reason="${3:-}"
@@ -8401,6 +8735,7 @@ _legacy_help() {
8401
8735
  echo " backlog block <pat> [reason] Mark matching items as 🔒 Blocked 标记为已阻塞"
8402
8736
  echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
8403
8737
  echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
8738
+ echo " backlog unstick [--dry-run] Revert In Progress whose cycle failed >4h ago 自愈卡住的进行中任务"
8404
8739
  echo " backlog lint Check descriptions for path/function/filename violations 检查描述合规"
8405
8740
  echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
8406
8741
  echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
@@ -8481,17 +8816,17 @@ _check_structure() {
8481
8816
  fi
8482
8817
 
8483
8818
  if [[ "$_has_old_path" == "true" ]] && _has_roll_signature "$root"; then
8484
- err "Legacy structure detected at: $root 发现老结构目录"
8819
+ err "$(msg check_structure.detected "$root")"
8485
8820
  echo "" >&2
8486
- echo " This project uses the pre-2.0 layout (BACKLOG.md / docs/*). Roll 2.0 requires" >&2
8487
- echo " process artifacts to live in .roll/. Run the migration to upgrade:" >&2
8821
+ echo " $(msg check_structure.pre_2_0_layout)" >&2
8822
+ echo " $(msg check_structure.run_migration)" >&2
8488
8823
  echo "" >&2
8489
- echo " roll migrate --dry-run # Preview changes" >&2
8490
- echo " roll migrate # Execute (single atomic commit)" >&2
8824
+ echo " roll migrate --dry-run $(msg check_structure.preview_changes)" >&2
8825
+ echo " roll migrate $(msg check_structure.execute)" >&2
8491
8826
  echo "" >&2
8492
- echo " Migration guide: ${ROLL_PKG_DIR}/guide/en/migration-2.0.md" >&2
8827
+ echo " $(msg check_structure.migration_guide): ${ROLL_PKG_DIR}/guide/en/migration-2.0.md" >&2
8493
8828
  echo "" >&2
8494
- echo " To roll back to Roll 1.x temporarily:" >&2
8829
+ echo " $(msg check_structure.roll_back):" >&2
8495
8830
  echo " npm install -g @seanyao/roll@1" >&2
8496
8831
  exit 1
8497
8832
  fi
@@ -8526,11 +8861,12 @@ main() {
8526
8861
  review-pr) cmd_review_pr "$@" ;;
8527
8862
  slides) cmd_slides "$@" ;;
8528
8863
  prices) cmd_prices "$@" ;;
8864
+ changelog) cmd_changelog "$@" ;;
8529
8865
  version|--version|-v) echo "roll v${VERSION}" ;;
8530
8866
  help|--help|-h) _help "$@" ;;
8531
8867
  "") [[ -f ".roll/backlog.md" ]] && _home || { _help; _show_changelog; } ;;
8532
8868
  *)
8533
- err "Unknown command: $cmd 未知命令: $cmd"
8869
+ err "$(msg main.unknown_command "$cmd")"
8534
8870
  echo ""
8535
8871
  usage
8536
8872
  exit 1
@@ -8543,7 +8879,7 @@ _show_changelog() {
8543
8879
  local changelog="${ROLL_PKG_DIR}/CHANGELOG.md"
8544
8880
  [[ -f "$changelog" ]] || return 0
8545
8881
 
8546
- echo -e "${BOLD}Recent Changes 最近更新:${NC}"
8882
+ echo -e "${BOLD}$(msg changelog.heading):${NC}"
8547
8883
 
8548
8884
  local count=0 in_section=false
8549
8885
  while IFS= read -r line; do
@@ -8589,7 +8925,7 @@ _notify_update() {
8589
8925
  return
8590
8926
  fi
8591
8927
  echo ""
8592
- warn "v${latest} available — run 'roll update' to upgrade 有新版本 v${latest} — 运行 'roll update' 升级"
8928
+ warn "$(msg update.available "$latest")"
8593
8929
  }
8594
8930
 
8595
8931
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then