@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.
- package/CHANGELOG.md +15 -0
- package/bin/roll +411 -75
- package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-home.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/changelog_audit.py +155 -0
- package/lib/i18n/agent.sh +0 -0
- package/lib/i18n/alert.sh +0 -0
- package/lib/i18n/backlog.sh +0 -0
- package/lib/i18n/brief.sh +0 -0
- package/lib/i18n/changelog.sh +0 -0
- package/lib/i18n/ci.sh +0 -0
- package/lib/i18n/debug.sh +0 -0
- package/lib/i18n/doctor.sh +31 -0
- package/lib/i18n/dream.sh +0 -0
- package/lib/i18n/init.sh +17 -0
- package/lib/i18n/lang.sh +0 -0
- package/lib/i18n/loop.sh +0 -0
- package/lib/i18n/migrate.sh +0 -0
- package/lib/i18n/offboard.sh +15 -0
- package/lib/i18n/onboard.sh +0 -0
- package/lib/i18n/peer.sh +0 -0
- package/lib/i18n/propose.sh +0 -0
- package/lib/i18n/release.sh +0 -0
- package/lib/i18n/research.sh +0 -0
- package/lib/i18n/review_pr.sh +0 -0
- package/lib/i18n/sentinel.sh +0 -0
- package/lib/i18n/setup.sh +0 -0
- package/lib/i18n/shared.sh +83 -0
- package/lib/i18n/skills/roll-brief.sh +20 -0
- package/lib/i18n/skills/roll-build.sh +97 -0
- package/lib/i18n/skills/roll-design.sh +18 -0
- package/lib/i18n/skills/roll-fix.sh +14 -0
- package/lib/i18n/skills/roll-loop.sh +28 -0
- package/lib/i18n/skills/roll-onboard.sh +16 -0
- package/lib/i18n/slides.sh +0 -0
- package/lib/i18n/status.sh +0 -0
- package/lib/i18n/update.sh +9 -0
- package/lib/i18n.sh +25 -0
- package/lib/loop-fmt.py +12 -8
- package/lib/loop_unstick.py +180 -0
- package/lib/model_prices.py +93 -12
- package/lib/prices/snapshot-2026-05-22.json +2 -0
- package/lib/prices/snapshot-2026-05-23-deepseek.json +15 -0
- package/lib/prices/snapshot-2026-05-23-kimi.json +14 -0
- package/lib/roll-home.py +17 -1
- package/lib/roll-loop-status.py +3 -0
- package/lib/roll_render.py +5 -2
- package/package.json +1 -1
- 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.
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
949
|
+
echo " $(msg doctor.pr_double_gate_enabled)"
|
|
949
950
|
;;
|
|
950
951
|
disabled)
|
|
951
|
-
echo "
|
|
952
|
+
echo " $(msg doctor.pr_double_gate_disabled)"
|
|
952
953
|
_print_pr_pipeline_hint
|
|
953
954
|
;;
|
|
954
955
|
*)
|
|
955
|
-
echo "
|
|
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 "
|
|
963
|
+
echo " $(msg doctor.pr_event_enabled)"
|
|
963
964
|
;;
|
|
964
965
|
*)
|
|
965
|
-
echo "
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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 "
|
|
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 "
|
|
1038
|
+
warn "$(msg update.still_mismatch "$after")"
|
|
1041
1039
|
fi
|
|
1042
1040
|
fi
|
|
1043
1041
|
}
|
|
1044
1042
|
|
|
1045
1043
|
cmd_update() {
|
|
1046
|
-
info "
|
|
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 "
|
|
1521
|
+
err "$(msg init.onboard_cancelled)" >&2
|
|
1524
1522
|
else
|
|
1525
|
-
err "
|
|
1523
|
+
err "$(msg init.onboard_agent_exited "$agent" "$code")" >&2
|
|
1526
1524
|
fi
|
|
1527
1525
|
echo "" >&2
|
|
1528
|
-
echo "
|
|
1529
|
-
echo " -
|
|
1530
|
-
echo " -
|
|
1531
|
-
echo " -
|
|
1532
|
-
echo " -
|
|
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 "
|
|
1825
|
-
err "
|
|
1822
|
+
err "$(msg offboard.no_changeset_en)"
|
|
1823
|
+
err "$(msg offboard.no_changeset_zh)"
|
|
1826
1824
|
echo "" >&2
|
|
1827
|
-
echo "
|
|
1828
|
-
echo "
|
|
1829
|
-
echo "
|
|
1830
|
-
echo "
|
|
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
|
|
3225
|
-
|
|
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
|
|
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
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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
|
-
#
|
|
3619
|
-
|
|
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"
|
|
3658
|
-
|
|
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':<
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 "
|
|
8819
|
+
err "$(msg check_structure.detected "$root")"
|
|
8485
8820
|
echo "" >&2
|
|
8486
|
-
echo "
|
|
8487
|
-
echo "
|
|
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
|
|
8490
|
-
echo " roll migrate
|
|
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 "
|
|
8827
|
+
echo " $(msg check_structure.migration_guide): ${ROLL_PKG_DIR}/guide/en/migration-2.0.md" >&2
|
|
8493
8828
|
echo "" >&2
|
|
8494
|
-
echo "
|
|
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 "
|
|
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}
|
|
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 "
|
|
8928
|
+
warn "$(msg update.available "$latest")"
|
|
8593
8929
|
}
|
|
8594
8930
|
|
|
8595
8931
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|