@seanyao/roll 2026.523.2 → 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 +28 -0
- package/bin/roll +546 -97
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- 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 +77 -11
- 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 +9 -0
- package/lib/roll_render.py +10 -2
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +4 -4
- package/skills/roll-loop/SKILL.md +15 -1
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.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"
|
|
@@ -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,18 +3339,22 @@ 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
|
-
|
|
3226
|
-
roll slides list List all decks
|
|
3227
|
-
列出 .roll/slides/
|
|
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
|
|
3345
|
+
roll slides list List all decks (built / stale / failed / unbuilt)
|
|
3346
|
+
列出 .roll/slides/ 下所有幻灯片(四态)
|
|
3228
3347
|
roll slides preview <slug> [--no-open]
|
|
3229
3348
|
Open .roll/slides/<slug>.html in the default browser
|
|
3230
3349
|
在浏览器中打开已渲染的幻灯片
|
|
3350
|
+
roll slides logs <slug> Show the last build failure log for a deck
|
|
3351
|
+
显示幻灯片上次构建失败日志
|
|
3231
3352
|
|
|
3232
3353
|
OPTIONS 选项
|
|
3233
3354
|
--no-open Skip auto-opening the rendered HTML in a browser
|
|
3234
3355
|
渲染后不自动打开浏览器
|
|
3356
|
+
--no-build Skip auto-build after agent completes (deck.md only)
|
|
3357
|
+
仅生成 deck.md,不自动渲染
|
|
3235
3358
|
--help, -h Show this help
|
|
3236
3359
|
显示本帮助
|
|
3237
3360
|
EOF
|
|
@@ -3246,6 +3369,12 @@ _slides_lib() {
|
|
|
3246
3369
|
# Returns 0 + prints the path if the template exists, else returns 1.
|
|
3247
3370
|
_slides_template_path() {
|
|
3248
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
|
|
3249
3378
|
local tpl="${ROLL_PKG_DIR}/lib/slides/templates/${name}.html"
|
|
3250
3379
|
if [[ -f "$tpl" ]]; then
|
|
3251
3380
|
printf '%s' "$tpl"
|
|
@@ -3338,8 +3467,19 @@ cmd_slides_build() {
|
|
|
3338
3467
|
return 1
|
|
3339
3468
|
fi
|
|
3340
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
|
+
|
|
3341
3473
|
# 1. Validate first (fail-fast on AI-generated decks).
|
|
3342
|
-
|
|
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"
|
|
3343
3483
|
err "deck.md validation failed — fix the issues above before building."
|
|
3344
3484
|
echo " deck.md 校验失败,请先修复上方提示再重试。" >&2
|
|
3345
3485
|
return 1
|
|
@@ -3349,16 +3489,25 @@ cmd_slides_build() {
|
|
|
3349
3489
|
local tpl_name; tpl_name=$(_slides_template_for_deck "$deck")
|
|
3350
3490
|
local tpl_path
|
|
3351
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"
|
|
3352
3495
|
err "Template not found: ${tpl_name} 未找到模板:${tpl_name}"
|
|
3353
3496
|
return 1
|
|
3354
3497
|
fi
|
|
3355
3498
|
|
|
3356
3499
|
local out=".roll/slides/${slug}.html"
|
|
3357
3500
|
mkdir -p ".roll/slides"
|
|
3358
|
-
|
|
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"
|
|
3359
3505
|
err "Render failed for ${deck} 渲染失败:${deck}"
|
|
3360
3506
|
return 1
|
|
3361
|
-
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
# US-DECK-011: build succeeded — remove any stale .last-build.err
|
|
3510
|
+
rm -f "$err_file" 2>/dev/null || true
|
|
3362
3511
|
|
|
3363
3512
|
# 3. Default-ignore the HTML artefact so it doesn't accidentally get committed.
|
|
3364
3513
|
_slides_ensure_gitignore
|
|
@@ -3450,31 +3599,42 @@ cmd_slides_list() {
|
|
|
3450
3599
|
IFS=$'\n' sorted_slugs=($(printf '%s\n' "${slugs[@]}" | sort))
|
|
3451
3600
|
unset IFS
|
|
3452
3601
|
|
|
3453
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3602
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3454
3603
|
"slug" "template" "total_slides" "created" "built" "size"
|
|
3455
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3456
|
-
"----" "--------" "------------" "-------" "
|
|
3604
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3605
|
+
"----" "--------" "------------" "-------" "------" "----"
|
|
3457
3606
|
|
|
3458
|
-
local s deck html template total created built size bytes
|
|
3607
|
+
local s deck html err_file template total created built size bytes
|
|
3459
3608
|
for s in "${sorted_slugs[@]}"; do
|
|
3460
3609
|
deck="${slides_dir}/${s}/deck.md"
|
|
3461
3610
|
html="${slides_dir}/${s}.html"
|
|
3611
|
+
err_file="${slides_dir}/${s}/.last-build.err"
|
|
3462
3612
|
template=$(_slides_frontmatter_field "$deck" "template")
|
|
3463
3613
|
[[ -z "$template" ]] && template="-"
|
|
3464
3614
|
total=$(_slides_frontmatter_field "$deck" "total_slides")
|
|
3465
3615
|
[[ -z "$total" ]] && total="-"
|
|
3466
3616
|
created=$(_slides_frontmatter_field "$deck" "created")
|
|
3467
3617
|
[[ -z "$created" ]] && created="-"
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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
|
|
3473
3633
|
else
|
|
3474
|
-
built="✗"
|
|
3634
|
+
built="✗ unbuilt"
|
|
3475
3635
|
size="-"
|
|
3476
3636
|
fi
|
|
3477
|
-
printf '%-20s %-20s %-12s %-12s %-
|
|
3637
|
+
printf '%-20s %-20s %-12s %-12s %-8s %s\n' \
|
|
3478
3638
|
"$s" "$template" "$total" "$created" "$built" "$size"
|
|
3479
3639
|
done
|
|
3480
3640
|
return 0
|
|
@@ -3524,6 +3684,46 @@ cmd_slides_preview() {
|
|
|
3524
3684
|
return 0
|
|
3525
3685
|
}
|
|
3526
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
|
+
|
|
3527
3727
|
# ─── US-DECK-004 ─────────────────────────────────────────────────────────────
|
|
3528
3728
|
# Turn a topic string into a kebab-case slug.
|
|
3529
3729
|
# Lower-cases, replaces any run of non-alphanumerics with a single dash,
|
|
@@ -3546,13 +3746,15 @@ _slides_topic_slug() {
|
|
|
3546
3746
|
# `.roll/slides/<slug>/deck.md` (and nothing else). After the agent exits,
|
|
3547
3747
|
# we print a bilingual hint pointing at `roll slides build <slug>`.
|
|
3548
3748
|
cmd_slides_new() {
|
|
3549
|
-
local topic="" template="introduction-v3"
|
|
3749
|
+
local topic="" template="introduction-v3" quiet=0 no_build=0
|
|
3550
3750
|
while [[ $# -gt 0 ]]; do
|
|
3551
3751
|
case "$1" in
|
|
3552
3752
|
--template)
|
|
3553
3753
|
[[ -n "${2:-}" ]] || { err "--template requires a value --template 需要一个值"; return 1; }
|
|
3554
3754
|
template="$2"; shift 2 ;;
|
|
3555
3755
|
--template=*) template="${1#--template=}"; shift ;;
|
|
3756
|
+
--quiet) quiet=1; shift ;;
|
|
3757
|
+
--no-build) no_build=1; shift ;;
|
|
3556
3758
|
--help|-h) _slides_help; return 0 ;;
|
|
3557
3759
|
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3558
3760
|
*)
|
|
@@ -3566,8 +3768,8 @@ cmd_slides_new() {
|
|
|
3566
3768
|
done
|
|
3567
3769
|
|
|
3568
3770
|
if [[ -z "$topic" ]]; then
|
|
3569
|
-
err "Usage: roll slides new \"<topic>\" [--template <name>]"
|
|
3570
|
-
echo " 用法:roll slides new \"<主题>\" [--template <模板名>]" >&2
|
|
3771
|
+
err "Usage: roll slides new \"<topic>\" [--template <name>] [--quiet] [--no-build]"
|
|
3772
|
+
echo " 用法:roll slides new \"<主题>\" [--template <模板名>] [--quiet] [--no-build]" >&2
|
|
3571
3773
|
return 1
|
|
3572
3774
|
fi
|
|
3573
3775
|
|
|
@@ -3578,14 +3780,26 @@ cmd_slides_new() {
|
|
|
3578
3780
|
return 1
|
|
3579
3781
|
fi
|
|
3580
3782
|
|
|
3783
|
+
# Progress: init with 5 phases, detect quiet mode
|
|
3784
|
+
_slides_progress_init launching generating validating rendering opening
|
|
3785
|
+
_slides_progress_detect_tty
|
|
3786
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3787
|
+
_slides_progress_quiet
|
|
3788
|
+
fi
|
|
3789
|
+
|
|
3790
|
+
# Ctrl-C trap: stop spinner and clean cursor
|
|
3791
|
+
trap '_slides_progress_phase_enter "cancelled" 2>/dev/null; exit 130' INT TERM
|
|
3792
|
+
|
|
3793
|
+
# Phase: launching
|
|
3794
|
+
_slides_progress_phase_enter "launching"
|
|
3795
|
+
|
|
3581
3796
|
local skill_file="${ROLL_PKG_DIR}/skills/roll-deck/SKILL.md"
|
|
3582
3797
|
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill_file}"; return 1; }
|
|
3583
3798
|
local skill_body; skill_body=$(_skill_content "$skill_file")
|
|
3584
3799
|
|
|
3585
3800
|
local agent; agent=$(_project_agent)
|
|
3586
3801
|
|
|
3587
|
-
# Compose the full prompt
|
|
3588
|
-
# reads the skill, then sees the topic / slug / template it must use.
|
|
3802
|
+
# Compose the full prompt
|
|
3589
3803
|
local prompt
|
|
3590
3804
|
prompt="$(cat <<EOF
|
|
3591
3805
|
${skill_body}
|
|
@@ -3607,16 +3821,81 @@ EOF
|
|
|
3607
3821
|
|
|
3608
3822
|
_agent_argv "$agent" text "$prompt" || {
|
|
3609
3823
|
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
3824
|
+
trap - INT TERM
|
|
3610
3825
|
return 1
|
|
3611
3826
|
}
|
|
3612
3827
|
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3828
|
+
# Phase: generating — launch agent in background + spinner + file-watch
|
|
3829
|
+
_slides_progress_phase_enter "generating"
|
|
3830
|
+
|
|
3831
|
+
local deck_file=".roll/slides/${slug}/deck.md"
|
|
3832
|
+
local deck_dir; deck_dir="$(dirname "$deck_file")"
|
|
3833
|
+
mkdir -p "$deck_dir"
|
|
3834
|
+
|
|
3835
|
+
# Start spinner
|
|
3836
|
+
_slides_progress_spinner_start
|
|
3837
|
+
|
|
3838
|
+
# Launch agent in background
|
|
3839
|
+
"${_AGENT_ARGV[@]}" &
|
|
3840
|
+
local agent_pid=$!
|
|
3841
|
+
|
|
3842
|
+
# File-watch: poll deck.md for ## Slide N count and update spinner label
|
|
3843
|
+
local slide_count=0 last_slide_count=-1
|
|
3844
|
+
while kill -0 "$agent_pid" 2>/dev/null; do
|
|
3845
|
+
if [[ -f "$deck_file" ]]; then
|
|
3846
|
+
slide_count=$(grep -c '^## Slide ' "$deck_file" 2>/dev/null || echo 0)
|
|
3847
|
+
if [[ "$slide_count" != "$last_slide_count" ]]; then
|
|
3848
|
+
last_slide_count="$slide_count"
|
|
3849
|
+
if [[ "$slide_count" -gt 0 ]]; then
|
|
3850
|
+
_slides_progress_spinner_label "generating slide ${slide_count}/18"
|
|
3851
|
+
fi
|
|
3852
|
+
fi
|
|
3853
|
+
fi
|
|
3854
|
+
sleep 1
|
|
3855
|
+
done
|
|
3856
|
+
|
|
3857
|
+
# Agent finished — stop spinner
|
|
3858
|
+
if [[ -n "${_SLIDES_PROGRESS_SPINNER_PID:-}" ]]; then
|
|
3859
|
+
kill "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
|
|
3860
|
+
wait "${_SLIDES_PROGRESS_SPINNER_PID}" 2>/dev/null || true
|
|
3861
|
+
_SLIDES_PROGRESS_SPINNER_PID=""
|
|
3862
|
+
[[ $_SLIDES_PROGRESS_QUIET -eq 0 ]] && printf '\r\033[K' >&2
|
|
3863
|
+
fi
|
|
3864
|
+
|
|
3865
|
+
wait "$agent_pid"
|
|
3616
3866
|
local rc=$?
|
|
3617
3867
|
|
|
3618
|
-
#
|
|
3619
|
-
|
|
3868
|
+
# Phase: validating
|
|
3869
|
+
_slides_progress_phase_enter "validating"
|
|
3870
|
+
|
|
3871
|
+
# If deck.md exists, validate it
|
|
3872
|
+
if [[ -f "$deck_file" ]]; then
|
|
3873
|
+
local lib_dir; lib_dir=$(_slides_lib)
|
|
3874
|
+
if python3 "${lib_dir}/slides-validate.py" "$deck_file" >/dev/null 2>&1; then
|
|
3875
|
+
: # validation passed
|
|
3876
|
+
else
|
|
3877
|
+
[[ $_SLIDES_PROGRESS_QUIET -eq 0 ]] && printf ' ⚠️ validation warnings (build may surface details)\n' >&2
|
|
3878
|
+
fi
|
|
3879
|
+
fi
|
|
3880
|
+
|
|
3881
|
+
# Phase: rendering + opening (unless --no-build)
|
|
3882
|
+
if [[ "$no_build" -eq 0 ]] && [[ -f "$deck_file" ]]; then
|
|
3883
|
+
_slides_progress_phase_enter "rendering"
|
|
3884
|
+
cmd_slides_build "$slug" --no-open
|
|
3885
|
+
local build_rc=$?
|
|
3886
|
+
|
|
3887
|
+
if [[ $build_rc -eq 0 ]]; then
|
|
3888
|
+
_slides_progress_phase_enter "opening"
|
|
3889
|
+
local opener
|
|
3890
|
+
if opener=$(_slides_open_cmd); then
|
|
3891
|
+
"$opener" ".roll/slides/${slug}.html" >/dev/null 2>&1 || true
|
|
3892
|
+
fi
|
|
3893
|
+
fi
|
|
3894
|
+
fi
|
|
3895
|
+
|
|
3896
|
+
trap - INT TERM
|
|
3897
|
+
|
|
3898
|
+
# Print Next hint (unless --quiet flag was explicitly passed)
|
|
3620
3899
|
echo
|
|
3621
3900
|
echo "Next: roll slides build ${slug}"
|
|
3622
3901
|
echo "下一步:roll slides build ${slug}"
|
|
@@ -3624,14 +3903,15 @@ EOF
|
|
|
3624
3903
|
return "$rc"
|
|
3625
3904
|
}
|
|
3626
3905
|
|
|
3627
|
-
# ─── cmd_prices (US-VIEW-013)
|
|
3906
|
+
# ─── cmd_prices (US-VIEW-013 / FIX-116) ───────────────────────────────────────
|
|
3628
3907
|
# `roll prices show` — print the current price snapshot table.
|
|
3629
3908
|
# `roll prices refresh` — fetch live pricing docs, diff vs latest snapshot,
|
|
3630
3909
|
# write a new snapshot when rates have changed.
|
|
3910
|
+
# FIX-116: --vendor flag for multi-vendor support.
|
|
3631
3911
|
_prices_help() {
|
|
3632
3912
|
cat <<'EOF'
|
|
3633
|
-
Usage: roll prices <subcommand> [--url URL]
|
|
3634
|
-
roll prices <子命令> [--url 网址]
|
|
3913
|
+
Usage: roll prices <subcommand> [--url URL] [--vendor VENDOR]
|
|
3914
|
+
roll prices <子命令> [--url 网址] [--vendor 厂商]
|
|
3635
3915
|
|
|
3636
3916
|
Subcommands:
|
|
3637
3917
|
show Print the current price snapshot table.
|
|
@@ -3639,6 +3919,10 @@ Subcommands:
|
|
|
3639
3919
|
refresh Fetch the official pricing docs, diff against the latest snapshot,
|
|
3640
3920
|
and write a new snapshot only when rates have changed.
|
|
3641
3921
|
拉取官方价格文档与最新快照对比,有变化才落新快照。
|
|
3922
|
+
|
|
3923
|
+
Options:
|
|
3924
|
+
--vendor anthropic|deepseek|kimi Target vendor for refresh (default: anthropic).
|
|
3925
|
+
指定拉取价格的厂商(默认:anthropic)。
|
|
3642
3926
|
EOF
|
|
3643
3927
|
}
|
|
3644
3928
|
|
|
@@ -3654,36 +3938,60 @@ version, effective_at, source_url = mp.snapshot_meta()
|
|
|
3654
3938
|
print(f"price snapshot 价格快照")
|
|
3655
3939
|
print(f" version {version}")
|
|
3656
3940
|
print(f" effective_at {effective_at}")
|
|
3657
|
-
print(f"
|
|
3658
|
-
|
|
3941
|
+
print(f" snapshots {len(mp._SNAPSHOTS)} loaded 已加载")
|
|
3942
|
+
for snap in mp._SNAPSHOTS:
|
|
3943
|
+
v = snap.get("vendor", "—")
|
|
3944
|
+
c = snap.get("currency", "—")
|
|
3945
|
+
print(f" {v:<12} {c:>4} {snap['source_url']}")
|
|
3659
3946
|
print()
|
|
3660
|
-
print(f" {'model':<
|
|
3947
|
+
print(f" {'model':<24}{'cur':>4}{'in':>10}{'out':>10}{'cw':>10}{'cr':>10}")
|
|
3661
3948
|
for model in sorted(mp.PRICES):
|
|
3662
3949
|
p = mp.PRICES[model]
|
|
3663
|
-
|
|
3950
|
+
cur = mp.currency_for(model)
|
|
3951
|
+
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
3952
|
print()
|
|
3665
|
-
print("rates per million tokens
|
|
3953
|
+
print("rates per million tokens 每百万 token 单价")
|
|
3666
3954
|
PY
|
|
3667
3955
|
}
|
|
3668
3956
|
|
|
3669
3957
|
cmd_prices_refresh() {
|
|
3670
3958
|
local url=""
|
|
3959
|
+
local vendor=""
|
|
3671
3960
|
while (( $# > 0 )); do
|
|
3672
3961
|
case "$1" in
|
|
3673
3962
|
--url) url="$2"; shift 2 ;;
|
|
3963
|
+
--vendor) vendor="$2"; shift 2 ;;
|
|
3674
3964
|
*) err "Unknown flag: $1 未知参数: $1"; return 1 ;;
|
|
3675
3965
|
esac
|
|
3676
3966
|
done
|
|
3677
3967
|
local lib_dir="${ROLL_PKG_DIR}/lib"
|
|
3678
|
-
python3 - "$lib_dir" "$url" <<'PY'
|
|
3968
|
+
python3 - "$lib_dir" "$url" "$vendor" <<'PY'
|
|
3679
3969
|
import os, sys
|
|
3680
3970
|
lib_dir = sys.argv[1]
|
|
3681
3971
|
override_url = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
3972
|
+
vendor = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
3682
3973
|
sys.path.insert(0, lib_dir)
|
|
3683
3974
|
import prices_fetcher as pf
|
|
3684
3975
|
|
|
3685
3976
|
snapshot_dir = os.path.join(lib_dir, "prices")
|
|
3686
|
-
|
|
3977
|
+
|
|
3978
|
+
# FIX-116: vendor-specific default URLs.
|
|
3979
|
+
VENDOR_URLS = {
|
|
3980
|
+
"anthropic": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
3981
|
+
"deepseek": "https://api-docs.deepseek.com/quick_start/pricing",
|
|
3982
|
+
"kimi": "https://platform.kimi.com/docs/pricing/chat",
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
if vendor:
|
|
3986
|
+
default_url = VENDOR_URLS.get(vendor)
|
|
3987
|
+
if not default_url:
|
|
3988
|
+
print(f"[roll] unknown vendor: {vendor} 未知厂商", file=sys.stderr)
|
|
3989
|
+
print(f"[roll] known vendors: {', '.join(sorted(VENDOR_URLS))} 已知厂商", file=sys.stderr)
|
|
3990
|
+
sys.exit(1)
|
|
3991
|
+
url = override_url or default_url
|
|
3992
|
+
pf.DEFAULT_SOURCE_URL = url
|
|
3993
|
+
else:
|
|
3994
|
+
url = override_url or pf.DEFAULT_SOURCE_URL
|
|
3687
3995
|
|
|
3688
3996
|
try:
|
|
3689
3997
|
action, changes = pf.refresh(snapshot_dir=snapshot_dir, url=url)
|
|
@@ -3725,6 +4033,37 @@ cmd_prices() {
|
|
|
3725
4033
|
esac
|
|
3726
4034
|
}
|
|
3727
4035
|
|
|
4036
|
+
# FIX-113: changelog audit — list PRs merged to main since latest release
|
|
4037
|
+
# tag that don't appear in CHANGELOG.md's ## Unreleased section.
|
|
4038
|
+
cmd_changelog() {
|
|
4039
|
+
local subcmd="${1:-audit}"
|
|
4040
|
+
shift || true
|
|
4041
|
+
case "$subcmd" in
|
|
4042
|
+
audit)
|
|
4043
|
+
python3 "${ROLL_PKG_DIR}/lib/changelog_audit.py" "$@"
|
|
4044
|
+
;;
|
|
4045
|
+
--help|-h|help)
|
|
4046
|
+
cat <<EOF
|
|
4047
|
+
Usage: roll changelog audit [--since <tag>] [--json]
|
|
4048
|
+
发版前体检
|
|
4049
|
+
Scan PRs merged to main since latest v* tag; flag ones that look user-visible
|
|
4050
|
+
(carry US-/FIX-/REFACTOR- id) but are missing from CHANGELOG.md '## Unreleased'.
|
|
4051
|
+
|
|
4052
|
+
Read-only — never edits CHANGELOG. Use to catch drift before release.
|
|
4053
|
+
|
|
4054
|
+
roll changelog audit # check against latest v* tag
|
|
4055
|
+
roll changelog audit --since v2026.520.1
|
|
4056
|
+
roll changelog audit --json # machine-readable
|
|
4057
|
+
EOF
|
|
4058
|
+
;;
|
|
4059
|
+
*)
|
|
4060
|
+
err "Unknown subcommand: ${subcmd} 未知子命令:${subcmd}"
|
|
4061
|
+
err "Try: roll changelog audit"
|
|
4062
|
+
return 1
|
|
4063
|
+
;;
|
|
4064
|
+
esac
|
|
4065
|
+
}
|
|
4066
|
+
|
|
3728
4067
|
cmd_slides() {
|
|
3729
4068
|
local subcmd="${1:-}"
|
|
3730
4069
|
shift || true
|
|
@@ -3741,6 +4080,9 @@ cmd_slides() {
|
|
|
3741
4080
|
preview)
|
|
3742
4081
|
cmd_slides_preview "$@"
|
|
3743
4082
|
;;
|
|
4083
|
+
logs)
|
|
4084
|
+
cmd_slides_logs "$@"
|
|
4085
|
+
;;
|
|
3744
4086
|
--help|-h|help)
|
|
3745
4087
|
_slides_help
|
|
3746
4088
|
return 0
|
|
@@ -4428,6 +4770,21 @@ _write_loop_runner_script() {
|
|
|
4428
4770
|
claude_cmd="${claude_cmd/--output-format stream-json/--output-format stream-json --add-dir \"\$WT\"}"
|
|
4429
4771
|
fi
|
|
4430
4772
|
local slug; slug=$(_project_slug "$project_path")
|
|
4773
|
+
# FIX-115: build fallback agent command. When primary agent fails all 3
|
|
4774
|
+
# attempts, the inner runner switches to this command for 3 more attempts.
|
|
4775
|
+
local fallback_agent; fallback_agent=$(_fallback_agent)
|
|
4776
|
+
local fallback_cmd=""
|
|
4777
|
+
if [[ -n "$fallback_agent" ]]; then
|
|
4778
|
+
local fallback_skill_file="${ROLL_HOME}/skills/roll-loop/SKILL.md"
|
|
4779
|
+
if [[ -f "$fallback_skill_file" ]]; then
|
|
4780
|
+
local fallback_prompt; fallback_prompt=$(_skill_content "$fallback_skill_file")
|
|
4781
|
+
if _agent_argv "$fallback_agent" plain "$fallback_prompt" 2>/dev/null; then
|
|
4782
|
+
_AGENT_ARGV[0]=$(command -v "${_AGENT_ARGV[0]}" 2>/dev/null || echo "${_AGENT_ARGV[0]}")
|
|
4783
|
+
printf -v fallback_cmd '%q ' "${_AGENT_ARGV[@]}"
|
|
4784
|
+
fallback_cmd="${fallback_cmd% }"
|
|
4785
|
+
fi
|
|
4786
|
+
fi
|
|
4787
|
+
fi
|
|
4431
4788
|
cat > "$inner_path" << INNER
|
|
4432
4789
|
#!/bin/bash -l
|
|
4433
4790
|
set -o pipefail
|
|
@@ -4755,6 +5112,18 @@ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
|
4755
5112
|
[ -d "\${_orphan_wt}/.git" ] || [ -f "\${_orphan_wt}/.git" ] || continue
|
|
4756
5113
|
_orphan_branch=\$(cd "\$_orphan_wt" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
4757
5114
|
[ -z "\$_orphan_branch" ] && continue
|
|
5115
|
+
# FIX-114: PR for this branch may already be squash-merged externally. In
|
|
5116
|
+
# that case origin/main..HEAD still shows commits (squash created new SHA)
|
|
5117
|
+
# so the old "needs republish" path tries to recreate the PR and fails.
|
|
5118
|
+
# Ask gh first; if MERGED → drop the worktree clean.
|
|
5119
|
+
if command -v gh >/dev/null 2>&1; then
|
|
5120
|
+
_orphan_pr_state=\$(gh pr view "\$_orphan_branch" --json state -q .state 2>/dev/null || echo "")
|
|
5121
|
+
if [ "\$_orphan_pr_state" = "MERGED" ]; then
|
|
5122
|
+
echo "[loop] FIX-114: orphan worktree \$_orphan_wt branch \$_orphan_branch already merged remotely; cleaning up"
|
|
5123
|
+
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
5124
|
+
continue
|
|
5125
|
+
fi
|
|
5126
|
+
fi
|
|
4758
5127
|
_orphan_commits=\$(cd "\$_orphan_wt" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
4759
5128
|
if [ "\$_orphan_commits" -gt 0 ]; then
|
|
4760
5129
|
echo "[loop] FIX-040: recovering orphan worktree \$_orphan_wt (branch \$_orphan_branch, \${_orphan_commits} commits)"
|
|
@@ -4796,6 +5165,7 @@ if _worktree_fetch_origin main \\
|
|
|
4796
5165
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
4797
5166
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
4798
5167
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
5168
|
+
_loop_event agent_used "\${CYCLE_ID}" "\$(_project_agent)" "primary" || true
|
|
4799
5169
|
_phase_end worktree_setup ok
|
|
4800
5170
|
else
|
|
4801
5171
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
@@ -4820,7 +5190,14 @@ FMT="${fmt_script}"
|
|
|
4820
5190
|
export LOOP_PROJECT_SLUG="${slug}"
|
|
4821
5191
|
export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
4822
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)"
|
|
4823
5196
|
_phase_begin claude_invoke
|
|
5197
|
+
# FIX-115: fallback agent support — when primary fails 3 attempts, try fallback 3 more
|
|
5198
|
+
FALLBACK_AGENT_NAME="${fallback_agent}"
|
|
5199
|
+
FALLBACK_CMD="${fallback_cmd}"
|
|
5200
|
+
_USE_FALLBACK=0
|
|
4824
5201
|
for _attempt in 1 2 3; do
|
|
4825
5202
|
# FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
|
|
4826
5203
|
# the SIGTERM result of the previous attempt and would otherwise force an
|
|
@@ -4858,6 +5235,44 @@ for _attempt in 1 2 3; do
|
|
|
4858
5235
|
fi
|
|
4859
5236
|
done
|
|
4860
5237
|
|
|
5238
|
+
# FIX-115: fallback agent retry — when primary fails all 3 attempts and a
|
|
5239
|
+
# fallback_agent is configured, try the fallback for 3 more attempts.
|
|
5240
|
+
if [ "\$_CYCLE_TIMED_OUT" -eq 0 ] && [ "\$_exit" -ne 0 ] && [ -n "\$FALLBACK_AGENT_NAME" ] && [ -n "\$FALLBACK_CMD" ]; then
|
|
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
|
|
5243
|
+
echo "[loop] primary agent failed after 3 attempts — switching to fallback: \$FALLBACK_AGENT_NAME"
|
|
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"
|
|
5248
|
+
for _attempt in 1 2 3; do
|
|
5249
|
+
_CYCLE_TIMED_OUT=0
|
|
5250
|
+
( sleep "\$LOOP_CYCLE_TIMEOUT_SEC" && {
|
|
5251
|
+
kill -TERM \$\$ 2>/dev/null
|
|
5252
|
+
pkill -TERM -P \$\$ 2>/dev/null
|
|
5253
|
+
pkill -TERM -f "\$WT" 2>/dev/null
|
|
5254
|
+
sleep 5
|
|
5255
|
+
pkill -KILL -P \$\$ 2>/dev/null
|
|
5256
|
+
pkill -KILL -f "\$WT" 2>/dev/null
|
|
5257
|
+
} ) &
|
|
5258
|
+
_WATCHDOG_PID=\$!
|
|
5259
|
+
if [ -f "\$FMT" ]; then
|
|
5260
|
+
( cd "\$WT" && \$FALLBACK_CMD ) | python3 "\$FMT"
|
|
5261
|
+
else
|
|
5262
|
+
( cd "\$WT" && \$FALLBACK_CMD )
|
|
5263
|
+
fi
|
|
5264
|
+
_exit=\$?
|
|
5265
|
+
kill "\$_WATCHDOG_PID" 2>/dev/null
|
|
5266
|
+
wait "\$_WATCHDOG_PID" 2>/dev/null
|
|
5267
|
+
[ "\$_CYCLE_TIMED_OUT" -eq 1 ] && break
|
|
5268
|
+
[ "\$_exit" -eq 0 ] && break
|
|
5269
|
+
if [ "\$_attempt" -lt 3 ]; then
|
|
5270
|
+
echo "[loop] \$FALLBACK_AGENT_NAME exited \$_exit (attempt \$_attempt/3) — retrying in 30s..."
|
|
5271
|
+
sleep 30
|
|
5272
|
+
fi
|
|
5273
|
+
done
|
|
5274
|
+
fi
|
|
5275
|
+
|
|
4861
5276
|
if [ "\$_CYCLE_TIMED_OUT" -eq 1 ] || [ "\$_exit" -ne 0 ]; then
|
|
4862
5277
|
_phase_end claude_invoke fail
|
|
4863
5278
|
else
|
|
@@ -5316,7 +5731,7 @@ cmd_loop() {
|
|
|
5316
5731
|
on) _loop_on ;;
|
|
5317
5732
|
off) _loop_off ;;
|
|
5318
5733
|
now) _loop_now ;;
|
|
5319
|
-
test) _loop_test ;;
|
|
5734
|
+
test) shift; _loop_test "$@" ;;
|
|
5320
5735
|
status) _loop_status "$@" ;;
|
|
5321
5736
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
5322
5737
|
runs) _loop_runs "$@" ;;
|
|
@@ -5531,17 +5946,36 @@ _loop_test() {
|
|
|
5531
5946
|
local log="${_SHARED_ROOT}/loop/cron-${slug}.log"
|
|
5532
5947
|
local test_runner="${_SHARED_ROOT}/loop/run-${slug}-test.sh"
|
|
5533
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
|
+
|
|
5534
5968
|
# FIX-054: terminal preference removed — runner always uses Terminal.app.
|
|
5535
5969
|
local active_start active_end
|
|
5536
5970
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
5537
5971
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
5538
5972
|
|
|
5539
|
-
info "Generating test runner...
|
|
5973
|
+
info "Generating test runner (agent: ${agent})... 正在生成测试启动脚本 (agent: ${agent})..."
|
|
5540
5974
|
_write_loop_runner_script "$test_runner" "$project_path" \
|
|
5541
|
-
|
|
5975
|
+
"${agent_cmd}" \
|
|
5542
5976
|
"$log" "$active_start" "$active_end"
|
|
5543
5977
|
|
|
5544
|
-
info "Starting smoke test (
|
|
5978
|
+
info "Starting smoke test (agent: ${agent})... 正在运行 smoke 测试 (agent: ${agent})..."
|
|
5545
5979
|
info "Watch for: tmux session + terminal popup + stream-json events flowing"
|
|
5546
5980
|
info "观察:tmux 会话 + 终端弹窗 + stream-json 事件流"
|
|
5547
5981
|
|
|
@@ -5551,9 +5985,9 @@ _loop_test() {
|
|
|
5551
5985
|
local elapsed=$(( $(date +%s) - start_time ))
|
|
5552
5986
|
|
|
5553
5987
|
if [[ $exit_code -eq 0 ]]; then
|
|
5554
|
-
ok "Smoke test passed (${elapsed}s) smoke 测试通过 (${elapsed}
|
|
5988
|
+
ok "Smoke test passed (${elapsed}s, agent: ${agent}) smoke 测试通过 (${elapsed}秒, agent: ${agent})"
|
|
5555
5989
|
else
|
|
5556
|
-
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})"
|
|
5557
5991
|
return 1
|
|
5558
5992
|
fi
|
|
5559
5993
|
}
|
|
@@ -6296,8 +6730,13 @@ _loop_check_depends_on() {
|
|
|
6296
6730
|
}
|
|
6297
6731
|
|
|
6298
6732
|
# _loop_is_manual_only <story-id> [backlog-path]
|
|
6299
|
-
# Exit 0: story's own row carries
|
|
6733
|
+
# Exit 0: story's own row carries any manual-only:<value> tag.
|
|
6300
6734
|
# Exit 1: tag absent, story-id not found, or backlog missing.
|
|
6735
|
+
#
|
|
6736
|
+
# FIX-109: loop used to accept only the literal `manual-only:true` value;
|
|
6737
|
+
# anything else (e.g. `manual-only:roll-meta`, `manual-only:sean-yao`) was
|
|
6738
|
+
# ignored and the story got picked. Now any non-empty `manual-only:<value>`
|
|
6739
|
+
# qualifies — lets users tag rows reserved for a specific human or scope.
|
|
6301
6740
|
_loop_is_manual_only() {
|
|
6302
6741
|
local id="$1"
|
|
6303
6742
|
local backlog="${2:-.roll/backlog.md}"
|
|
@@ -6308,7 +6747,7 @@ _loop_is_manual_only() {
|
|
|
6308
6747
|
row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
|
|
6309
6748
|
[ -n "$row" ] || return 1
|
|
6310
6749
|
|
|
6311
|
-
echo "$row" | grep -qE 'manual-only:
|
|
6750
|
+
echo "$row" | grep -qE 'manual-only:[A-Za-z0-9_.-]+'
|
|
6312
6751
|
}
|
|
6313
6752
|
|
|
6314
6753
|
# US-AUTO-034: PR-first inbox — loop processes open PRs before scanning BACKLOG.
|
|
@@ -7865,6 +8304,14 @@ cmd_backlog() {
|
|
|
7865
8304
|
_backlog_lint "$@" "$backlog"
|
|
7866
8305
|
return
|
|
7867
8306
|
;;
|
|
8307
|
+
unstick)
|
|
8308
|
+
# FIX-112: revert 🔨 In Progress stories whose latest cycle ended
|
|
8309
|
+
# failed / aborted / blocked > N hours ago (default 4). Conservative
|
|
8310
|
+
# gate so it never undoes legitimately-in-progress work.
|
|
8311
|
+
shift
|
|
8312
|
+
python3 "${ROLL_PKG_DIR}/lib/loop_unstick.py" "$@"
|
|
8313
|
+
return
|
|
8314
|
+
;;
|
|
7868
8315
|
block|defer|unblock|promote)
|
|
7869
8316
|
local pattern="${2:-}"
|
|
7870
8317
|
local reason="${3:-}"
|
|
@@ -8401,6 +8848,7 @@ _legacy_help() {
|
|
|
8401
8848
|
echo " backlog block <pat> [reason] Mark matching items as 🔒 Blocked 标记为已阻塞"
|
|
8402
8849
|
echo " backlog defer <pat> [reason] Mark matching items as ⏸ Deferred 标记为已推迟"
|
|
8403
8850
|
echo " backlog unblock <pat> Restore matching items to 📋 Todo 恢复为待处理"
|
|
8851
|
+
echo " backlog unstick [--dry-run] Revert In Progress whose cycle failed >4h ago 自愈卡住的进行中任务"
|
|
8404
8852
|
echo " backlog lint Check descriptions for path/function/filename violations 检查描述合规"
|
|
8405
8853
|
echo " agent [use <name>|list] [Config] Per-project agent selection 切换项目 agent"
|
|
8406
8854
|
echo " ci [--wait] [CI] Show or wait for current commit's CI status 查看/等待 CI 状态"
|
|
@@ -8481,17 +8929,17 @@ _check_structure() {
|
|
|
8481
8929
|
fi
|
|
8482
8930
|
|
|
8483
8931
|
if [[ "$_has_old_path" == "true" ]] && _has_roll_signature "$root"; then
|
|
8484
|
-
err "
|
|
8932
|
+
err "$(msg check_structure.detected "$root")"
|
|
8485
8933
|
echo "" >&2
|
|
8486
|
-
echo "
|
|
8487
|
-
echo "
|
|
8934
|
+
echo " $(msg check_structure.pre_2_0_layout)" >&2
|
|
8935
|
+
echo " $(msg check_structure.run_migration)" >&2
|
|
8488
8936
|
echo "" >&2
|
|
8489
|
-
echo " roll migrate --dry-run
|
|
8490
|
-
echo " roll migrate
|
|
8937
|
+
echo " roll migrate --dry-run $(msg check_structure.preview_changes)" >&2
|
|
8938
|
+
echo " roll migrate $(msg check_structure.execute)" >&2
|
|
8491
8939
|
echo "" >&2
|
|
8492
|
-
echo "
|
|
8940
|
+
echo " $(msg check_structure.migration_guide): ${ROLL_PKG_DIR}/guide/en/migration-2.0.md" >&2
|
|
8493
8941
|
echo "" >&2
|
|
8494
|
-
echo "
|
|
8942
|
+
echo " $(msg check_structure.roll_back):" >&2
|
|
8495
8943
|
echo " npm install -g @seanyao/roll@1" >&2
|
|
8496
8944
|
exit 1
|
|
8497
8945
|
fi
|
|
@@ -8526,11 +8974,12 @@ main() {
|
|
|
8526
8974
|
review-pr) cmd_review_pr "$@" ;;
|
|
8527
8975
|
slides) cmd_slides "$@" ;;
|
|
8528
8976
|
prices) cmd_prices "$@" ;;
|
|
8977
|
+
changelog) cmd_changelog "$@" ;;
|
|
8529
8978
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
8530
8979
|
help|--help|-h) _help "$@" ;;
|
|
8531
8980
|
"") [[ -f ".roll/backlog.md" ]] && _home || { _help; _show_changelog; } ;;
|
|
8532
8981
|
*)
|
|
8533
|
-
err "
|
|
8982
|
+
err "$(msg main.unknown_command "$cmd")"
|
|
8534
8983
|
echo ""
|
|
8535
8984
|
usage
|
|
8536
8985
|
exit 1
|
|
@@ -8543,7 +8992,7 @@ _show_changelog() {
|
|
|
8543
8992
|
local changelog="${ROLL_PKG_DIR}/CHANGELOG.md"
|
|
8544
8993
|
[[ -f "$changelog" ]] || return 0
|
|
8545
8994
|
|
|
8546
|
-
echo -e "${BOLD}
|
|
8995
|
+
echo -e "${BOLD}$(msg changelog.heading):${NC}"
|
|
8547
8996
|
|
|
8548
8997
|
local count=0 in_section=false
|
|
8549
8998
|
while IFS= read -r line; do
|
|
@@ -8589,7 +9038,7 @@ _notify_update() {
|
|
|
8589
9038
|
return
|
|
8590
9039
|
fi
|
|
8591
9040
|
echo ""
|
|
8592
|
-
warn "
|
|
9041
|
+
warn "$(msg update.available "$latest")"
|
|
8593
9042
|
}
|
|
8594
9043
|
|
|
8595
9044
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|