@seanyao/roll 2026.524.2 → 2026.526.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 +38 -8
- package/README.md +4 -2
- package/bin/roll +1022 -442
- package/conventions/config.yaml +9 -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/i18n/agent.sh +21 -0
- package/lib/i18n/alert.sh +20 -0
- package/lib/i18n/backlog.sh +96 -0
- package/lib/i18n/brief.sh +5 -0
- package/lib/i18n/changelog.sh +3 -0
- package/lib/i18n/ci.sh +15 -0
- package/lib/i18n/init.sh +52 -0
- package/lib/i18n/lang.sh +10 -0
- package/lib/i18n/loop.sh +140 -0
- package/lib/i18n/migrate.sh +74 -0
- package/lib/i18n/offboard.sh +16 -0
- package/lib/i18n/peer.sh +34 -0
- package/lib/i18n/peer_help.sh +21 -0
- package/lib/i18n/peer_reset.sh +7 -0
- package/lib/i18n/peer_status.sh +5 -0
- package/lib/i18n/prices.sh +3 -0
- package/lib/i18n/prices_refresh.sh +17 -0
- package/lib/i18n/prices_show.sh +7 -0
- package/lib/i18n/setup.sh +3 -0
- package/lib/i18n/shared.sh +74 -0
- package/lib/i18n/skills/roll-brief.sh +27 -0
- package/lib/i18n/skills/roll-fix.sh +39 -0
- package/lib/i18n/skills/roll-onboard.sh +17 -0
- package/lib/i18n/slides.sh +3 -0
- package/lib/i18n/slides_build.sh +38 -0
- package/lib/i18n/slides_delete.sh +19 -0
- package/lib/i18n/slides_list.sh +14 -0
- package/lib/i18n/slides_logs.sh +12 -0
- package/lib/i18n/slides_new.sh +15 -0
- package/lib/i18n/slides_preview.sh +14 -0
- package/lib/i18n/slides_templates.sh +7 -0
- package/lib/i18n/status.sh +19 -0
- package/lib/i18n/update.sh +7 -0
- package/lib/i18n.sh +85 -4
- package/lib/roll-home.py +55 -0
- package/lib/roll-loop-status.py +196 -19
- package/lib/roll-loop-story.py +191 -0
- package/lib/roll_render.py +15 -1
- package/lib/slides/components/README.md +117 -0
- package/lib/slides/components/cards-2.html +9 -0
- package/lib/slides/components/cards-3.html +9 -0
- package/lib/slides/components/cards-4.html +9 -0
- package/lib/slides/components/compare.html +22 -0
- package/lib/slides/components/highlight.html +9 -0
- package/lib/slides/components/pipeline.html +12 -0
- package/lib/slides/components/plain.html +7 -0
- package/lib/slides/components/quote.html +4 -0
- package/lib/slides/components/timeline.html +9 -0
- package/lib/slides/templates/pitch.html +0 -0
- package/package.json +1 -1
- package/skills/roll-brief/SKILL.md +2 -2
- package/skills/roll-build/SKILL.md +40 -40
- package/skills/roll-fix/SKILL.md +22 -22
- package/skills/roll-loop/SKILL.md +26 -15
- package/skills/roll-onboard/SKILL.md +6 -6
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.526.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"
|
|
@@ -25,6 +25,10 @@ ROLL_PKG_CONVENTIONS="${ROLL_PKG_DIR}/conventions"
|
|
|
25
25
|
# shellcheck source=../lib/i18n.sh
|
|
26
26
|
[[ -f "${ROLL_PKG_DIR}/lib/i18n.sh" ]] && source "${ROLL_PKG_DIR}/lib/i18n.sh"
|
|
27
27
|
|
|
28
|
+
# US-WATCH-001: upstream compatibility watch helpers.
|
|
29
|
+
# shellcheck source=../lib/watch.sh
|
|
30
|
+
[[ -f "${ROLL_PKG_DIR}/lib/watch.sh" ]] && source "${ROLL_PKG_DIR}/lib/watch.sh"
|
|
31
|
+
|
|
28
32
|
# Colors
|
|
29
33
|
RED=$'\033[0;31m'
|
|
30
34
|
GREEN=$'\033[0;32m'
|
|
@@ -292,13 +296,13 @@ _ensure_config_entries() {
|
|
|
292
296
|
echo "${key}: ${val}" >> "$tmp"
|
|
293
297
|
fi
|
|
294
298
|
added=$((added + 1))
|
|
295
|
-
warn "
|
|
299
|
+
warn "$(msg shared.added_missing_config_entry ${key})"
|
|
296
300
|
fi
|
|
297
301
|
done
|
|
298
302
|
|
|
299
303
|
if [[ $added -gt 0 ]]; then
|
|
300
304
|
cp "$tmp" "$ROLL_CONFIG"
|
|
301
|
-
ok "
|
|
305
|
+
ok "$(msg shared.config_updated_with_new_entries $added)"
|
|
302
306
|
fi
|
|
303
307
|
rm -f "$tmp"
|
|
304
308
|
}
|
|
@@ -332,11 +336,11 @@ safe_copy() {
|
|
|
332
336
|
# hidden read or silently default to overwrite. Be explicit.
|
|
333
337
|
if [[ ! -t 0 ]]; then
|
|
334
338
|
cp "$src" "$dst"
|
|
335
|
-
ok "
|
|
339
|
+
ok "$(msg shared.wrote ${dst/#$HOME/~})"
|
|
336
340
|
return
|
|
337
341
|
fi
|
|
338
342
|
echo ""
|
|
339
|
-
warn "
|
|
343
|
+
warn "$(msg shared.file_exists_and_differs ${dst/#$HOME/~})"
|
|
340
344
|
echo -e " ${BOLD}Overwrite?${NC} [Y/n/d(iff)] "
|
|
341
345
|
read -r answer || answer="Y"
|
|
342
346
|
case "$answer" in
|
|
@@ -345,15 +349,15 @@ safe_copy() {
|
|
|
345
349
|
echo ""
|
|
346
350
|
echo -e " ${BOLD}Overwrite?${NC} [Y/n] "
|
|
347
351
|
read -r answer2 || answer2="Y"
|
|
348
|
-
[[ "$answer2" =~ ^[Nn]$ ]] && { info "
|
|
352
|
+
[[ "$answer2" =~ ^[Nn]$ ]] && { info "$(msg shared.skipped ${dst/#$HOME/~})"; return; }
|
|
349
353
|
;;
|
|
350
|
-
n|N) info "
|
|
354
|
+
n|N) info "$(msg shared.skipped ${dst/#$HOME/~})"; return ;;
|
|
351
355
|
*) ;; # empty answer or 'y' / 'Y' → overwrite (default Yes)
|
|
352
356
|
esac
|
|
353
357
|
fi
|
|
354
358
|
|
|
355
359
|
cp "$src" "$dst"
|
|
356
|
-
ok "
|
|
360
|
+
ok "$(msg shared.wrote_2 ${dst/#$HOME/~})"
|
|
357
361
|
}
|
|
358
362
|
|
|
359
363
|
# ─── Internal: prune files in $1 that no longer exist in $2 ──────────────────
|
|
@@ -372,7 +376,7 @@ _prune_dir() {
|
|
|
372
376
|
[[ "$installed_fname" == "." || "$installed_fname" == ".." ]] && continue
|
|
373
377
|
if [[ ! -f "$source_dir/$installed_fname" ]]; then
|
|
374
378
|
rm -f "$installed_f"
|
|
375
|
-
info "
|
|
379
|
+
info "$(msg shared.removed_stale $label ${installed_dir##*/} $installed_fname)"
|
|
376
380
|
fi
|
|
377
381
|
done
|
|
378
382
|
}
|
|
@@ -380,7 +384,7 @@ _prune_dir() {
|
|
|
380
384
|
# ─── Internal: pull skills from repo → ~/.roll/skills ──────────────────────
|
|
381
385
|
_pull_skills() {
|
|
382
386
|
if [[ ! -d "$ROLL_PKG_DIR/skills" ]]; then
|
|
383
|
-
err "
|
|
387
|
+
err "$(msg shared.skills_source_not_found_at_skills $ROLL_PKG_DIR)"
|
|
384
388
|
return 1
|
|
385
389
|
fi
|
|
386
390
|
|
|
@@ -408,7 +412,7 @@ _pull_skills() {
|
|
|
408
412
|
installed_name="$(basename "$installed_dir")"
|
|
409
413
|
if [[ ! -d "$ROLL_PKG_DIR/skills/$installed_name" ]]; then
|
|
410
414
|
rm -rf "$installed_dir"
|
|
411
|
-
info "
|
|
415
|
+
info "$(msg shared.removed_stale_skill $installed_name)"
|
|
412
416
|
fi
|
|
413
417
|
done
|
|
414
418
|
}
|
|
@@ -418,14 +422,14 @@ _pull_conventions() {
|
|
|
418
422
|
local force="${1:-false}"
|
|
419
423
|
|
|
420
424
|
if [[ ! -d "$ROLL_PKG_CONVENTIONS" ]]; then
|
|
421
|
-
err "
|
|
425
|
+
err "$(msg shared.convention_source_not_found_at $ROLL_PKG_CONVENTIONS)"
|
|
422
426
|
return 1
|
|
423
427
|
fi
|
|
424
428
|
|
|
425
429
|
mkdir -p "$ROLL_GLOBAL"
|
|
426
430
|
mkdir -p "$ROLL_TEMPLATES"/{fullstack,frontend-only,backend-service,cli}
|
|
427
431
|
|
|
428
|
-
info "
|
|
432
|
+
info "$(msg shared.copying_global_conventions)"
|
|
429
433
|
for f in "$ROLL_PKG_CONVENTIONS"/global/*; do
|
|
430
434
|
[[ -f "$f" ]] && safe_copy "$f" "$ROLL_GLOBAL/$(basename "$f")" "$force"
|
|
431
435
|
done
|
|
@@ -436,7 +440,7 @@ _pull_conventions() {
|
|
|
436
440
|
# Prune stale files in ~/.roll/conventions/global/
|
|
437
441
|
_prune_dir "$ROLL_GLOBAL" "$ROLL_PKG_CONVENTIONS/global" "convention"
|
|
438
442
|
|
|
439
|
-
info "
|
|
443
|
+
info "$(msg shared.copying_project_templates)"
|
|
440
444
|
for tpl_dir in "$ROLL_PKG_CONVENTIONS"/templates/*/; do
|
|
441
445
|
local tpl_name
|
|
442
446
|
tpl_name="$(basename "$tpl_dir")"
|
|
@@ -457,8 +461,8 @@ _install_local() {
|
|
|
457
461
|
local force="${1:-false}"
|
|
458
462
|
|
|
459
463
|
if [[ ! -d "$ROLL_PKG_CONVENTIONS" ]]; then
|
|
460
|
-
err "
|
|
461
|
-
err "
|
|
464
|
+
err "$(msg shared.convention_source_not_found_at_2 $ROLL_PKG_CONVENTIONS)"
|
|
465
|
+
err "$(msg shared.run_this_from_the_roll_repo)"
|
|
462
466
|
exit 1
|
|
463
467
|
fi
|
|
464
468
|
|
|
@@ -467,15 +471,15 @@ _install_local() {
|
|
|
467
471
|
|
|
468
472
|
# Recreate config if it has no ai_* entries (covers legacy sync_* format and blank/broken configs)
|
|
469
473
|
if [[ -f "$ROLL_CONFIG" ]] && ! grep -qE "^ai_[a-z]+:" "$ROLL_CONFIG" 2>/dev/null; then
|
|
470
|
-
warn "
|
|
474
|
+
warn "$(msg shared.config_has_no_ai_entries_recreating)"
|
|
471
475
|
cp "$ROLL_CONFIG" "${ROLL_CONFIG}.bak"
|
|
472
|
-
info "
|
|
476
|
+
info "$(msg shared.backup_saved_roll_config_yaml_bak)"
|
|
473
477
|
rm "$ROLL_CONFIG"
|
|
474
478
|
fi
|
|
475
479
|
|
|
476
480
|
# Create config if it doesn't exist
|
|
477
481
|
if [[ ! -f "$ROLL_CONFIG" ]]; then
|
|
478
|
-
info "
|
|
482
|
+
info "$(msg shared.creating_default_config)"
|
|
479
483
|
cat > "$ROLL_CONFIG" << 'YAML'
|
|
480
484
|
# Roll Configuration
|
|
481
485
|
# Edit this file, then run `roll setup` to apply.
|
|
@@ -509,7 +513,7 @@ loop_brief_hour: 9
|
|
|
509
513
|
# loop_brief_minute: 15 # omit to auto-derive
|
|
510
514
|
primary_agent: claude
|
|
511
515
|
YAML
|
|
512
|
-
ok "
|
|
516
|
+
ok "$(msg shared.created_roll_config_yaml)"
|
|
513
517
|
fi
|
|
514
518
|
|
|
515
519
|
# Ensure all expected ai_* keys exist (handles upgrades where new tools were added)
|
|
@@ -537,7 +541,7 @@ _link_skills() {
|
|
|
537
541
|
|
|
538
542
|
if [[ -n "$ai_dir_real" && \
|
|
539
543
|
( "$ai_dir_real" == "$ROLL_PKG_DIR" || "$ai_dir_real" == "$ROLL_PKG_DIR"/* ) ]]; then
|
|
540
|
-
warn "
|
|
544
|
+
warn "$(msg shared.skipped_resolves_to_repo_refusing_to ${ai_name})"
|
|
541
545
|
continue
|
|
542
546
|
fi
|
|
543
547
|
|
|
@@ -546,7 +550,7 @@ _link_skills() {
|
|
|
546
550
|
skills_real="$(canonical_dir "$skills_dir" 2>/dev/null || true)"
|
|
547
551
|
if [[ -n "$skills_real" && -n "$pkg_skills_real" && \
|
|
548
552
|
( "$skills_real" == "$pkg_skills_real" || "$skills_real" == "$pkg_skills_real"/* ) ]]; then
|
|
549
|
-
warn "
|
|
553
|
+
warn "$(msg shared.skipped_resolves_to_repo "${ai_name}" "$(lower_name "$ai_name")")"
|
|
550
554
|
continue
|
|
551
555
|
fi
|
|
552
556
|
|
|
@@ -559,10 +563,10 @@ _link_skills() {
|
|
|
559
563
|
fi
|
|
560
564
|
# Dangling whole-dir symlink — remove and recreate as per-skill links
|
|
561
565
|
if [[ -z "$skills_real" ]]; then
|
|
562
|
-
info "
|
|
566
|
+
info "$(msg shared.removing_legacy_symlink_skills ${ai_name} ${skills_target/#$HOME/~})"
|
|
563
567
|
rm "$skills_dir"
|
|
564
568
|
else
|
|
565
|
-
warn "
|
|
569
|
+
warn "$(msg shared.skipped_skills_unknown_symlink_target ${ai_name} ${skills_target/#$HOME/~})"
|
|
566
570
|
continue
|
|
567
571
|
fi
|
|
568
572
|
fi
|
|
@@ -571,7 +575,7 @@ _link_skills() {
|
|
|
571
575
|
skills_real="$(canonical_dir "$skills_dir" 2>/dev/null || true)"
|
|
572
576
|
if [[ -n "$skills_real" && -n "$pkg_skills_real" && \
|
|
573
577
|
( "$skills_real" == "$pkg_skills_real" || "$skills_real" == "$pkg_skills_real"/* ) ]]; then
|
|
574
|
-
warn "
|
|
578
|
+
warn "$(msg shared.skipped_skills_created_path_resolves_to ${ai_name})"
|
|
575
579
|
continue
|
|
576
580
|
fi
|
|
577
581
|
local linked=0 repaired=0 pruned=0
|
|
@@ -611,7 +615,7 @@ _link_skills() {
|
|
|
611
615
|
# real file/dir at that path: skip — never touch user content
|
|
612
616
|
done
|
|
613
617
|
if [[ $((linked + repaired + pruned)) -gt 0 ]]; then
|
|
614
|
-
ok "
|
|
618
|
+
ok "$(msg shared.skills_linked_in_skills_new_repaired ${ai_name} ${linked} ${repaired} ${pruned})"
|
|
615
619
|
fi
|
|
616
620
|
done < <(_get_ai_tools)
|
|
617
621
|
}
|
|
@@ -637,18 +641,18 @@ _sync_convention_for_tool() {
|
|
|
637
641
|
local wk_file="$dst_dir/roll.md"
|
|
638
642
|
if [[ "$force" == "true" ]] || ! diff -q "$src" "$wk_file" &>/dev/null 2>&1; then
|
|
639
643
|
cp "$src" "$wk_file"
|
|
640
|
-
ok "
|
|
644
|
+
ok "$(msg shared.wrote_3 ${wk_file/#$HOME/~})"
|
|
641
645
|
fi
|
|
642
646
|
|
|
643
647
|
# Append @roll.md include to main config — never overwrite existing content
|
|
644
648
|
if [[ ! -f "$main_dst" ]]; then
|
|
645
649
|
printf '@roll.md\n' > "$main_dst"
|
|
646
|
-
ok "
|
|
650
|
+
ok "$(msg shared.created ${main_dst/#$HOME/~})"
|
|
647
651
|
elif ! grep -qF "@roll.md" "$main_dst" 2>/dev/null; then
|
|
648
652
|
printf '\n@roll.md\n' >> "$main_dst"
|
|
649
|
-
ok "
|
|
653
|
+
ok "$(msg shared.appended_roll_md_to ${main_dst/#$HOME/~})"
|
|
650
654
|
else
|
|
651
|
-
ok "
|
|
655
|
+
ok "$(msg shared.already_included ${main_dst/#$HOME/~})"
|
|
652
656
|
fi
|
|
653
657
|
}
|
|
654
658
|
|
|
@@ -665,10 +669,10 @@ _sync_conventions() {
|
|
|
665
669
|
# ─── Internal: sync skills (pull + link) ──────────────────────────────────────
|
|
666
670
|
_sync_skills() {
|
|
667
671
|
local force="${1:-false}"
|
|
668
|
-
info "
|
|
672
|
+
info "$(msg shared.updating_skills)"
|
|
669
673
|
_pull_skills
|
|
670
|
-
ok "
|
|
671
|
-
info "
|
|
674
|
+
ok "$(msg shared.skills_updated_in_roll_skills)"
|
|
675
|
+
info "$(msg shared.creating_skill_symlinks_for_ai_tools)"
|
|
672
676
|
_link_skills "$force"
|
|
673
677
|
}
|
|
674
678
|
|
|
@@ -689,19 +693,19 @@ _ensure_tmux() {
|
|
|
689
693
|
local os; os="$(uname)"
|
|
690
694
|
if [[ "$os" == "Darwin" ]]; then
|
|
691
695
|
if command -v brew >/dev/null 2>&1; then
|
|
692
|
-
info "
|
|
696
|
+
info "$(msg shared.tmux_not_found_installing_via_brew)"
|
|
693
697
|
if brew install tmux >/dev/null 2>&1; then
|
|
694
|
-
ok "
|
|
698
|
+
ok "$(msg shared.tmux_installed_tmux)"
|
|
695
699
|
return 0
|
|
696
700
|
fi
|
|
697
|
-
warn "
|
|
701
|
+
warn "$(msg shared.brew_install_tmux_failed_install_manually)"
|
|
698
702
|
return 0
|
|
699
703
|
fi
|
|
700
|
-
warn "
|
|
704
|
+
warn "$(msg shared.tmux_required_but_brew_not_available)"
|
|
701
705
|
return 0
|
|
702
706
|
fi
|
|
703
707
|
|
|
704
|
-
warn "
|
|
708
|
+
warn "$(msg shared.tmux_required_install_via_your_package)"
|
|
705
709
|
return 0
|
|
706
710
|
}
|
|
707
711
|
|
|
@@ -756,7 +760,7 @@ cmd_setup() {
|
|
|
756
760
|
while [[ $# -gt 0 ]]; do
|
|
757
761
|
case "$1" in
|
|
758
762
|
--force|-f) force=true; shift ;;
|
|
759
|
-
*) err "
|
|
763
|
+
*) err "$(msg setup.unknown_argument_1)"; exit 1 ;;
|
|
760
764
|
esac
|
|
761
765
|
done
|
|
762
766
|
|
|
@@ -1042,18 +1046,18 @@ _check_installed_version_or_retry() {
|
|
|
1042
1046
|
|
|
1043
1047
|
cmd_update() {
|
|
1044
1048
|
info "$(msg update.current_version "$VERSION")"
|
|
1045
|
-
info "
|
|
1049
|
+
info "$(msg update.upgrading_via_npm)"
|
|
1046
1050
|
echo ""
|
|
1047
1051
|
|
|
1048
1052
|
if ! npm install -g @seanyao/roll@latest; then
|
|
1049
|
-
err "
|
|
1053
|
+
err "$(msg update.npm_install_failed_check_network_proxy)"
|
|
1050
1054
|
exit 1
|
|
1051
1055
|
fi
|
|
1052
1056
|
|
|
1053
1057
|
_check_installed_version_or_retry
|
|
1054
1058
|
|
|
1055
1059
|
echo ""
|
|
1056
|
-
info "
|
|
1060
|
+
info "$(msg update.re_syncing_to_ai_tools)"
|
|
1057
1061
|
echo ""
|
|
1058
1062
|
cmd_setup
|
|
1059
1063
|
|
|
@@ -1193,7 +1197,7 @@ cmd_init() {
|
|
|
1193
1197
|
# US-ONBOARD-009: --apply consumes onboard-plan.yaml produced by $roll-onboard
|
|
1194
1198
|
if [[ "${1:-}" == "--apply" ]]; then
|
|
1195
1199
|
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
1196
|
-
err "
|
|
1200
|
+
err "$(msg init.no_templates_found_run_roll_setup)"
|
|
1197
1201
|
exit 1
|
|
1198
1202
|
fi
|
|
1199
1203
|
shift
|
|
@@ -1202,12 +1206,12 @@ cmd_init() {
|
|
|
1202
1206
|
fi
|
|
1203
1207
|
|
|
1204
1208
|
if [[ "${1:-}" == -* ]]; then
|
|
1205
|
-
err "
|
|
1209
|
+
err "$(msg init.unknown_flag_1)"
|
|
1206
1210
|
exit 1
|
|
1207
1211
|
fi
|
|
1208
1212
|
|
|
1209
1213
|
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
1210
|
-
err "
|
|
1214
|
+
err "$(msg init.no_templates_found_run_roll_setup_2)"
|
|
1211
1215
|
exit 1
|
|
1212
1216
|
fi
|
|
1213
1217
|
|
|
@@ -1372,13 +1376,13 @@ _init_legacy_onboard_guide() {
|
|
|
1372
1376
|
local count_summary
|
|
1373
1377
|
count_summary=$(_init_legacy_file_summary "$project_dir")
|
|
1374
1378
|
|
|
1375
|
-
info "
|
|
1379
|
+
info "$(msg init.detected_legacy_project ${count_summary})"
|
|
1376
1380
|
echo ""
|
|
1377
1381
|
|
|
1378
1382
|
# Discover installed agents (writes to globals: _ONBOARD_INSTALLED, _ONBOARD_MISSING).
|
|
1379
1383
|
_onboard_discover_agents
|
|
1380
1384
|
|
|
1381
|
-
echo "
|
|
1385
|
+
echo "$(msg init.onboarding)"
|
|
1382
1386
|
echo " Onboarding requires an AI agent to read your code. Detected:"
|
|
1383
1387
|
echo ""
|
|
1384
1388
|
local n
|
|
@@ -1396,15 +1400,15 @@ _init_legacy_onboard_guide() {
|
|
|
1396
1400
|
if [[ ${#_ONBOARD_INSTALLED[@]} -eq 0 ]]; then
|
|
1397
1401
|
echo ""
|
|
1398
1402
|
err "No AI agent detected. Install one (e.g., 'claude', 'codex', 'kimi') and try again."
|
|
1399
|
-
err "
|
|
1403
|
+
err "$(msg init.no_ai_agent_detected_install_one)"
|
|
1400
1404
|
return 1
|
|
1401
1405
|
fi
|
|
1402
1406
|
|
|
1403
1407
|
echo ""
|
|
1404
|
-
echo "
|
|
1408
|
+
echo "$(msg init.the_process_will_use_your_agent)"
|
|
1405
1409
|
echo " Onboarding uses your agent to call models — tokens are billed to your account."
|
|
1406
1410
|
echo ""
|
|
1407
|
-
echo "
|
|
1411
|
+
echo "$(msg init.code_and_conversations_stay_in_your)"
|
|
1408
1412
|
echo " Your code and conversation stay in your agent — Roll never uploads anything."
|
|
1409
1413
|
echo ""
|
|
1410
1414
|
|
|
@@ -1416,9 +1420,9 @@ _init_legacy_onboard_guide() {
|
|
|
1416
1420
|
[[ -n "$chosen" ]] || return 1
|
|
1417
1421
|
|
|
1418
1422
|
echo ""
|
|
1419
|
-
info "
|
|
1423
|
+
info "$(msg init.launching ${chosen})"
|
|
1420
1424
|
echo " Conversation ends with /exit (or Ctrl-C). On exit Roll will run apply for you."
|
|
1421
|
-
echo "
|
|
1425
|
+
echo "$(msg init.use_exit_to_end_or_ctrl)"
|
|
1422
1426
|
echo ""
|
|
1423
1427
|
|
|
1424
1428
|
# US-ONBOARD-018: actually run the agent with the onboard prompt pre-loaded.
|
|
@@ -1475,7 +1479,7 @@ _onboard_select_agent() {
|
|
|
1475
1479
|
|
|
1476
1480
|
# Multi-agent: prompt the user. To stderr so stdout stays clean for the caller.
|
|
1477
1481
|
{
|
|
1478
|
-
echo "
|
|
1482
|
+
echo "$(msg init.pick_an_agent)"
|
|
1479
1483
|
local i=1
|
|
1480
1484
|
for c in "${candidates[@]}"; do
|
|
1481
1485
|
printf " %d) %s\n" "$i" "$c"
|
|
@@ -1486,11 +1490,11 @@ _onboard_select_agent() {
|
|
|
1486
1490
|
|
|
1487
1491
|
local choice
|
|
1488
1492
|
if ! IFS= read -r choice; then
|
|
1489
|
-
err "
|
|
1493
|
+
err "$(msg init.no_input_received_aborting_onboard)" >&2
|
|
1490
1494
|
return 1
|
|
1491
1495
|
fi
|
|
1492
1496
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#candidates[@]} )); then
|
|
1493
|
-
err "
|
|
1497
|
+
err "$(msg init.invalid_choice ${choice})" >&2
|
|
1494
1498
|
return 1
|
|
1495
1499
|
fi
|
|
1496
1500
|
printf '%s\n' "${candidates[$((choice - 1))]}"
|
|
@@ -1542,7 +1546,7 @@ _run_onboard_agent() {
|
|
|
1542
1546
|
local prompt
|
|
1543
1547
|
prompt=$(_onboard_initial_prompt) || return 1
|
|
1544
1548
|
_agent_argv "$agent" interactive "$prompt" || {
|
|
1545
|
-
err "
|
|
1549
|
+
err "$(msg init.agent_has_no_interactive_mode_wired ${agent})" >&2
|
|
1546
1550
|
return 1
|
|
1547
1551
|
}
|
|
1548
1552
|
|
|
@@ -1560,15 +1564,15 @@ _run_onboard_agent() {
|
|
|
1560
1564
|
if [[ ! -f "${project_dir}/.roll/onboard-plan.yaml" ]]; then
|
|
1561
1565
|
echo "" >&2
|
|
1562
1566
|
err "Agent exited cleanly but did not write .roll/onboard-plan.yaml." >&2
|
|
1563
|
-
err "
|
|
1567
|
+
err "$(msg init.agent)" >&2
|
|
1564
1568
|
echo " Re-run \`roll init\` once you've completed the conversation." >&2
|
|
1565
|
-
echo "
|
|
1569
|
+
echo "$(msg init.en_roll_init)" >&2
|
|
1566
1570
|
return 1
|
|
1567
1571
|
fi
|
|
1568
1572
|
|
|
1569
1573
|
# Plan present → chain into apply automatically.
|
|
1570
1574
|
echo "" >&2
|
|
1571
|
-
info "
|
|
1575
|
+
info "$(msg init.plan_written_running_apply)"
|
|
1572
1576
|
( cd "$project_dir" && _init_apply )
|
|
1573
1577
|
}
|
|
1574
1578
|
|
|
@@ -1687,27 +1691,27 @@ _init_apply() {
|
|
|
1687
1691
|
local validator="${ROLL_PKG_DIR}/lib/roll-plan-validate.py"
|
|
1688
1692
|
|
|
1689
1693
|
if [[ ! -f "$plan" ]]; then
|
|
1690
|
-
err "
|
|
1694
|
+
err "$(msg init.no_onboard_plan_found_at_roll)"
|
|
1691
1695
|
echo "" >&2
|
|
1692
1696
|
echo " Run \$roll-onboard in your AI agent first to generate the plan." >&2
|
|
1693
|
-
echo "
|
|
1697
|
+
echo "$(msg init.en_ai_agent_onboard_plan_ap $roll)" >&2
|
|
1694
1698
|
return 1
|
|
1695
1699
|
fi
|
|
1696
1700
|
|
|
1697
1701
|
if [[ ! -f "$validator" ]]; then
|
|
1698
|
-
err "
|
|
1702
|
+
err "$(msg init.plan_validator_missing $validator)"
|
|
1699
1703
|
return 1
|
|
1700
1704
|
fi
|
|
1701
1705
|
|
|
1702
1706
|
# Validate plan (schema + generated_at freshness + version)
|
|
1703
1707
|
if ! python3 "$validator" "$plan"; then
|
|
1704
|
-
err "
|
|
1708
|
+
err "$(msg init.plan_validation_failed_see_errors_above)"
|
|
1705
1709
|
echo "" >&2
|
|
1706
1710
|
echo " If the plan is stale (>24h), regenerate by running \$roll-onboard again." >&2
|
|
1707
1711
|
return 1
|
|
1708
1712
|
fi
|
|
1709
1713
|
|
|
1710
|
-
info "
|
|
1714
|
+
info "$(msg init.applying_onboard_plan)"
|
|
1711
1715
|
_ROLL_MERGE_SUMMARY=()
|
|
1712
1716
|
|
|
1713
1717
|
# US-ONBOARD-013: start a fresh changeset record so offboard can reverse.
|
|
@@ -1774,16 +1778,16 @@ print('true' if p.get('privacy', {}).get('gitignore_dot_roll', False) else 'fals
|
|
|
1774
1778
|
if ! grep -qFx ".roll/" "$gi" 2>/dev/null; then
|
|
1775
1779
|
echo ".roll/" >> "$gi"
|
|
1776
1780
|
_onboard_changeset_record "$project_dir" "gitignore_entries_added" ".roll/"
|
|
1777
|
-
ok "
|
|
1781
|
+
ok "$(msg init.added_roll_to_gitignore)"
|
|
1778
1782
|
fi
|
|
1779
1783
|
fi
|
|
1780
1784
|
|
|
1781
1785
|
echo ""
|
|
1782
|
-
info "
|
|
1786
|
+
info "$(msg init.syncing_conventions_to_ai_tools)"
|
|
1783
1787
|
_sync_conventions
|
|
1784
1788
|
echo ""
|
|
1785
1789
|
|
|
1786
|
-
ok "
|
|
1790
|
+
ok "$(msg init.onboard_apply_complete_onboard)"
|
|
1787
1791
|
}
|
|
1788
1792
|
|
|
1789
1793
|
# US-ONBOARD-014: roll offboard
|
|
@@ -1809,7 +1813,7 @@ cmd_offboard() {
|
|
|
1809
1813
|
return 0
|
|
1810
1814
|
;;
|
|
1811
1815
|
*)
|
|
1812
|
-
err "
|
|
1816
|
+
err "$(msg offboard.unknown_flag $arg)"
|
|
1813
1817
|
return 1
|
|
1814
1818
|
;;
|
|
1815
1819
|
esac
|
|
@@ -1848,7 +1852,7 @@ pr("launchd_plists_installed")
|
|
|
1848
1852
|
PY
|
|
1849
1853
|
)
|
|
1850
1854
|
if [[ $? -ne 0 ]]; then
|
|
1851
|
-
err "
|
|
1855
|
+
err "$(msg offboard.failed_to_parse_changeset)"
|
|
1852
1856
|
return 1
|
|
1853
1857
|
fi
|
|
1854
1858
|
|
|
@@ -1879,7 +1883,7 @@ PY
|
|
|
1879
1883
|
"$project_dir"|"$project_dir"/*) ;;
|
|
1880
1884
|
*)
|
|
1881
1885
|
err "Refusing to act on '$item' — it does not resolve under $project_dir"
|
|
1882
|
-
err "
|
|
1886
|
+
err "$(msg offboard.en $item)"
|
|
1883
1887
|
echo " This usually means the changeset was copied from another project." >&2
|
|
1884
1888
|
echo " Remove .roll/onboard-changeset.yaml manually, or rerun in the right dir." >&2
|
|
1885
1889
|
return 1
|
|
@@ -1913,19 +1917,19 @@ PY
|
|
|
1913
1917
|
fi
|
|
1914
1918
|
if [[ ${#files[@]} -eq 0 && ${#dirs[@]} -eq 0 && ${#gi_entries[@]} -eq 0 && ${#plists[@]} -eq 0 ]]; then
|
|
1915
1919
|
info "Changeset is empty — nothing to offboard."
|
|
1916
|
-
info "
|
|
1920
|
+
info "$(msg offboard.change_list_is_empty_nothing_to)"
|
|
1917
1921
|
return 0
|
|
1918
1922
|
fi
|
|
1919
1923
|
|
|
1920
1924
|
if [[ "$confirm" -ne 1 ]]; then
|
|
1921
1925
|
echo " This is a dry-run. Re-run with --confirm to apply."
|
|
1922
|
-
echo "
|
|
1926
|
+
echo "$(msg offboard.above_is_a_dry_run_preview)"
|
|
1923
1927
|
return 0
|
|
1924
1928
|
fi
|
|
1925
1929
|
|
|
1926
1930
|
# Apply. Guard every loop with a count check — `set -u` upstream makes
|
|
1927
1931
|
# naked `"${arr[@]}"` over an empty array a hard error on bash 5.0.
|
|
1928
|
-
echo "
|
|
1932
|
+
echo "$(msg offboard.applying_offboard)"
|
|
1929
1933
|
if [ "${#files[@]}" -gt 0 ]; then
|
|
1930
1934
|
for item in "${files[@]}"; do
|
|
1931
1935
|
rm -f "$project_dir/$item" 2>/dev/null && echo " removed file $item"
|
|
@@ -1955,7 +1959,7 @@ PY
|
|
|
1955
1959
|
fi
|
|
1956
1960
|
# Finally, remove the changeset file itself.
|
|
1957
1961
|
rm -f "$changeset"
|
|
1958
|
-
ok "
|
|
1962
|
+
ok "$(msg offboard.offboard_complete_offboard)"
|
|
1959
1963
|
}
|
|
1960
1964
|
|
|
1961
1965
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1978,13 +1982,13 @@ cmd_migrate() {
|
|
|
1978
1982
|
case "$1" in
|
|
1979
1983
|
--dry-run|-n) dry_run=true; shift ;;
|
|
1980
1984
|
-h|--help) _migrate_help; return 0 ;;
|
|
1981
|
-
*) err "
|
|
1985
|
+
*) err "$(msg migrate.unknown_arg_1 "$1")"; return 1 ;;
|
|
1982
1986
|
esac
|
|
1983
1987
|
done
|
|
1984
1988
|
|
|
1985
1989
|
# Must be in a git repo (git mv preserves history)
|
|
1986
1990
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1987
|
-
err "
|
|
1991
|
+
err "$(msg migrate.not_a_git_repository_roll_migrate)"
|
|
1988
1992
|
return 1
|
|
1989
1993
|
fi
|
|
1990
1994
|
|
|
@@ -2006,9 +2010,9 @@ cmd_migrate() {
|
|
|
2006
2010
|
|
|
2007
2011
|
# Three-state dispatch
|
|
2008
2012
|
if [[ "$has_new" == "true" && "$has_old" == "true" ]]; then
|
|
2009
|
-
err "
|
|
2013
|
+
err "$(msg migrate.both_old_and_new_structures_exist)"
|
|
2010
2014
|
echo "" >&2
|
|
2011
|
-
echo "
|
|
2015
|
+
echo "$(msg migrate.conflicting_paths)" >&2
|
|
2012
2016
|
for m in "${moves[@]}"; do
|
|
2013
2017
|
src="${m%%|*}"
|
|
2014
2018
|
local tgt="${m##*|}"
|
|
@@ -2017,17 +2021,17 @@ cmd_migrate() {
|
|
|
2017
2021
|
fi
|
|
2018
2022
|
done
|
|
2019
2023
|
echo "" >&2
|
|
2020
|
-
err "
|
|
2024
|
+
err "$(msg migrate.resolve_manually_then_re_run)"
|
|
2021
2025
|
return 1
|
|
2022
2026
|
fi
|
|
2023
2027
|
|
|
2024
2028
|
if [[ "$has_new" == "true" && "$has_old" == "false" ]]; then
|
|
2025
|
-
ok "
|
|
2029
|
+
ok "$(msg migrate.already_migrated_roll_exists_no_old)"
|
|
2026
2030
|
return 0
|
|
2027
2031
|
fi
|
|
2028
2032
|
|
|
2029
2033
|
if [[ "$has_old" == "false" ]]; then
|
|
2030
|
-
info "
|
|
2034
|
+
info "$(msg migrate.no_old_structure_detected_nothing_to)"
|
|
2031
2035
|
return 0
|
|
2032
2036
|
fi
|
|
2033
2037
|
|
|
@@ -2039,7 +2043,7 @@ cmd_migrate() {
|
|
|
2039
2043
|
done
|
|
2040
2044
|
|
|
2041
2045
|
if [[ ${#active_moves[@]} -eq 0 ]]; then
|
|
2042
|
-
warn "
|
|
2046
|
+
warn "$(msg migrate.old_structure_markers_found_but_no)"
|
|
2043
2047
|
return 0
|
|
2044
2048
|
fi
|
|
2045
2049
|
|
|
@@ -2050,7 +2054,7 @@ cmd_migrate() {
|
|
|
2050
2054
|
|
|
2051
2055
|
# Real execution requires clean working tree (we'll create a single commit)
|
|
2052
2056
|
if ! git diff --quiet --ignore-submodules HEAD 2>/dev/null; then
|
|
2053
|
-
err "
|
|
2057
|
+
err "$(msg migrate.working_tree_not_clean_commit_or)"
|
|
2054
2058
|
return 1
|
|
2055
2059
|
fi
|
|
2056
2060
|
|
|
@@ -2089,9 +2093,9 @@ EOF
|
|
|
2089
2093
|
}
|
|
2090
2094
|
|
|
2091
2095
|
_migrate_preview() {
|
|
2092
|
-
info "
|
|
2096
|
+
info "$(msg migrate.migration_preview_dry_run)"
|
|
2093
2097
|
echo ""
|
|
2094
|
-
printf " %-60s → %s\n" "
|
|
2098
|
+
printf " %-60s → %s\n" "$(msg migrate.old_path)" "$(msg migrate.new_path)"
|
|
2095
2099
|
local sep; sep=$(printf '─%.0s' {1..100})
|
|
2096
2100
|
printf " %s\n" "$sep"
|
|
2097
2101
|
local m
|
|
@@ -2100,11 +2104,11 @@ _migrate_preview() {
|
|
|
2100
2104
|
printf " %-60s → %s\n" "$src" "$tgt"
|
|
2101
2105
|
done
|
|
2102
2106
|
echo ""
|
|
2103
|
-
info "
|
|
2107
|
+
info "$(msg migrate.run_without_dry_run_to_execute)"
|
|
2104
2108
|
}
|
|
2105
2109
|
|
|
2106
2110
|
_migrate_execute() {
|
|
2107
|
-
info "
|
|
2111
|
+
info "$(msg migrate.migrating_paths_via_git_mv ${#@})"
|
|
2108
2112
|
local moved=0 m
|
|
2109
2113
|
for m in "$@"; do
|
|
2110
2114
|
local src="${m%%|*}" tgt="${m##*|}"
|
|
@@ -2112,7 +2116,7 @@ _migrate_execute() {
|
|
|
2112
2116
|
[[ -d "$target_dir" ]] || mkdir -p "$target_dir"
|
|
2113
2117
|
git mv "$src" "$tgt" || {
|
|
2114
2118
|
err "git mv failed: $src → $tgt"
|
|
2115
|
-
err "
|
|
2119
|
+
err "$(msg migrate.aborting_previous_moves_are_staged_but)"
|
|
2116
2120
|
return 1
|
|
2117
2121
|
}
|
|
2118
2122
|
moved=$((moved + 1))
|
|
@@ -2128,9 +2132,9 @@ Atomic migration via 'roll migrate' command. Process artifacts moved
|
|
|
2128
2132
|
from root and docs/ into .roll/; user docs relocated to guide/ and site/.
|
|
2129
2133
|
|
|
2130
2134
|
Paths migrated: ${moved}"
|
|
2131
|
-
ok "
|
|
2135
|
+
ok "$(msg migrate.migrated_paths_in_a_single_commit ${moved})"
|
|
2132
2136
|
echo ""
|
|
2133
|
-
echo "
|
|
2137
|
+
echo "$(msg migrate.next_steps)"
|
|
2134
2138
|
echo " git log -1 # Inspect the migration commit"
|
|
2135
2139
|
echo " roll status # Verify new structure"
|
|
2136
2140
|
}
|
|
@@ -2168,7 +2172,7 @@ print_merge_summary() {
|
|
|
2168
2172
|
return
|
|
2169
2173
|
fi
|
|
2170
2174
|
echo ""
|
|
2171
|
-
echo "
|
|
2175
|
+
echo "$(msg migrate.summary)"
|
|
2172
2176
|
for entry in "${_ROLL_MERGE_SUMMARY[@]}"; do
|
|
2173
2177
|
local action="${entry%%|*}"
|
|
2174
2178
|
local file="${entry##*|}"
|
|
@@ -2370,39 +2374,39 @@ EOF
|
|
|
2370
2374
|
# Show current state of conventions
|
|
2371
2375
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2372
2376
|
_legacy_status() {
|
|
2373
|
-
echo -e "${BOLD}
|
|
2377
|
+
echo -e "$(msg migrate.roll_convention_status_roll ${BOLD} ${NC})"
|
|
2374
2378
|
echo ""
|
|
2375
2379
|
|
|
2376
2380
|
if [[ -d "$ROLL_HOME" ]]; then
|
|
2377
|
-
ok "
|
|
2381
|
+
ok "$(msg migrate.roll_exists_roll)"
|
|
2378
2382
|
else
|
|
2379
|
-
err "
|
|
2383
|
+
err "$(msg migrate.roll_not_found_run_roll_setup)"
|
|
2380
2384
|
return
|
|
2381
2385
|
fi
|
|
2382
2386
|
|
|
2383
2387
|
echo ""
|
|
2384
|
-
echo -e "${BOLD}
|
|
2388
|
+
echo -e "$(msg migrate.global_conventions ${BOLD} ${NC})"
|
|
2385
2389
|
for f in AGENTS.md CLAUDE.md GEMINI.md .cursor-rules project_rules.md; do
|
|
2386
2390
|
if [[ -f "$ROLL_GLOBAL/$f" ]]; then
|
|
2387
2391
|
echo -e " ${GREEN}+${NC} $f"
|
|
2388
2392
|
else
|
|
2389
|
-
echo -e "
|
|
2393
|
+
echo -e "$(msg migrate.missing ${RED} ${NC} $f)"
|
|
2390
2394
|
fi
|
|
2391
2395
|
done
|
|
2392
2396
|
|
|
2393
2397
|
echo ""
|
|
2394
|
-
echo -e "${BOLD}
|
|
2398
|
+
echo -e "$(msg migrate.global_skills ${BOLD} ${NC})"
|
|
2395
2399
|
if [[ -d "$ROLL_HOME/skills" ]]; then
|
|
2396
2400
|
local count
|
|
2397
2401
|
count=$(find "$ROLL_HOME/skills" -maxdepth 1 -type d | wc -l | tr -d ' ')
|
|
2398
2402
|
count=$((count - 1))
|
|
2399
|
-
echo -e "
|
|
2403
|
+
echo -e "$(msg migrate.roll_skills_skills_installed ${GREEN} ${NC} $count)"
|
|
2400
2404
|
else
|
|
2401
|
-
echo -e "
|
|
2405
|
+
echo -e "$(msg migrate.roll_skills_missing ${RED} ${NC})"
|
|
2402
2406
|
fi
|
|
2403
2407
|
|
|
2404
2408
|
echo ""
|
|
2405
|
-
echo -e "${BOLD}
|
|
2409
|
+
echo -e "$(msg migrate.sync_targets ${BOLD} ${NC})"
|
|
2406
2410
|
|
|
2407
2411
|
local _sync_found=0
|
|
2408
2412
|
while IFS= read -r _entry; do
|
|
@@ -2415,12 +2419,12 @@ _legacy_status() {
|
|
|
2415
2419
|
check_sync_status "$_tool_name" "$ROLL_GLOBAL/$_src" "$_ai_d/$_cfg"
|
|
2416
2420
|
done < <(_get_ai_tools)
|
|
2417
2421
|
if [[ "$_sync_found" -eq 0 ]]; then
|
|
2418
|
-
warn "
|
|
2419
|
-
info "
|
|
2422
|
+
warn "$(msg migrate.no_ai_tools_configured_check_roll)"
|
|
2423
|
+
info "$(msg migrate.add_ai_entries_or_run_roll)"
|
|
2420
2424
|
fi
|
|
2421
2425
|
|
|
2422
2426
|
echo ""
|
|
2423
|
-
echo -e "${BOLD}
|
|
2427
|
+
echo -e "$(msg migrate.skill_symlinks ${BOLD} ${NC})"
|
|
2424
2428
|
local total_skills=0
|
|
2425
2429
|
local wk_skills_real
|
|
2426
2430
|
if [[ -d "$ROLL_HOME/skills" ]]; then
|
|
@@ -2446,7 +2450,7 @@ _legacy_status() {
|
|
|
2446
2450
|
skills_real="$(canonical_dir "$skills_dir" 2>/dev/null || true)"
|
|
2447
2451
|
local skills_display="${skills_dir/#$HOME/~}"
|
|
2448
2452
|
if [[ -n "$skills_real" && "$skills_real" == "$wk_skills_real" ]]; then
|
|
2449
|
-
echo -e "
|
|
2453
|
+
echo -e "$(msg migrate.roll_skills_mounted ${GREEN} ${NC} $name $skills_display)"
|
|
2450
2454
|
else
|
|
2451
2455
|
echo -e " ${YELLOW}~${NC} $name: $skills_display -> ${skills_target/#$HOME/~} (symlinked dir)"
|
|
2452
2456
|
fi
|
|
@@ -2461,25 +2465,25 @@ _legacy_status() {
|
|
|
2461
2465
|
elif [[ "$linked_count" -gt 0 ]]; then
|
|
2462
2466
|
echo -e " ${YELLOW}~${NC} $name: $skills_display ($linked_count/$total_skills skills linked)"
|
|
2463
2467
|
else
|
|
2464
|
-
echo -e "
|
|
2468
|
+
echo -e "$(msg migrate.no_roll_skills_linked ${RED} ${NC} $name $skills_display)"
|
|
2465
2469
|
fi
|
|
2466
2470
|
else
|
|
2467
|
-
echo -e "
|
|
2471
|
+
echo -e "$(msg migrate.not_found ${RED} ${NC} $name ${skills_dir/#$HOME/~})"
|
|
2468
2472
|
fi
|
|
2469
2473
|
done < <(_get_ai_tools)
|
|
2470
2474
|
if [[ "$_skills_found" -eq 0 ]]; then
|
|
2471
|
-
warn "
|
|
2475
|
+
warn "$(msg migrate.no_ai_tools_configured_check_roll_2)"
|
|
2472
2476
|
fi
|
|
2473
2477
|
|
|
2474
2478
|
echo ""
|
|
2475
|
-
echo -e "${BOLD}
|
|
2479
|
+
echo -e "$(msg migrate.templates ${BOLD} ${NC})"
|
|
2476
2480
|
for tpl in fullstack frontend-only backend-service cli; do
|
|
2477
2481
|
if [[ -d "$ROLL_TEMPLATES/$tpl" ]]; then
|
|
2478
2482
|
local count
|
|
2479
2483
|
count=$(find "$ROLL_TEMPLATES/$tpl" -type f | wc -l | tr -d ' ')
|
|
2480
2484
|
echo -e " ${GREEN}+${NC} $tpl ($count files)"
|
|
2481
2485
|
else
|
|
2482
|
-
echo -e "
|
|
2486
|
+
echo -e "$(msg migrate.missing_2 ${RED} ${NC} $tpl)"
|
|
2483
2487
|
fi
|
|
2484
2488
|
done
|
|
2485
2489
|
|
|
@@ -2505,7 +2509,7 @@ _status_loop_overview() {
|
|
|
2505
2509
|
[[ "${#plists[@]}" -eq 0 ]] && return 0
|
|
2506
2510
|
|
|
2507
2511
|
echo ""
|
|
2508
|
-
echo -e "${BOLD}
|
|
2512
|
+
echo -e "$(msg status.loop_overview ${BOLD} ${NC})"
|
|
2509
2513
|
|
|
2510
2514
|
for plist in "${plists[@]}"; do
|
|
2511
2515
|
local label; label=$(basename "$plist" .plist)
|
|
@@ -2563,15 +2567,15 @@ check_sync_status() {
|
|
|
2563
2567
|
# Sync writes content to {dir}/roll.md and appends @roll.md to the main config.
|
|
2564
2568
|
# So "in sync" means: roll.md exists + matches source + main config contains @roll.md.
|
|
2565
2569
|
if [[ ! -f "$dst" ]]; then
|
|
2566
|
-
echo -e "
|
|
2570
|
+
echo -e "$(msg status.not_synced ${RED} ${NC} $name $display)"
|
|
2567
2571
|
elif [[ ! -f "$wk_file" ]]; then
|
|
2568
|
-
echo -e "
|
|
2572
|
+
echo -e "$(msg status.out_of_sync_roll_md_missing ${YELLOW} ${NC} $name $display)"
|
|
2569
2573
|
elif ! diff -q "$src" "$wk_file" &>/dev/null 2>&1; then
|
|
2570
|
-
echo -e "
|
|
2574
|
+
echo -e "$(msg status.out_of_sync_roll_md_outdated ${YELLOW} ${NC} $name $display)"
|
|
2571
2575
|
elif ! grep -qF "@roll.md" "$dst" 2>/dev/null; then
|
|
2572
|
-
echo -e "
|
|
2576
|
+
echo -e "$(msg status.out_of_sync_roll_md_not ${YELLOW} ${NC} $name $display)"
|
|
2573
2577
|
else
|
|
2574
|
-
echo -e "
|
|
2578
|
+
echo -e "$(msg status.in_sync ${GREEN} ${NC} $name $display)"
|
|
2575
2579
|
fi
|
|
2576
2580
|
}
|
|
2577
2581
|
|
|
@@ -2674,7 +2678,13 @@ _peer_auto_attach() {
|
|
|
2674
2678
|
[ "$(uname)" = "Darwin" ] || return 0
|
|
2675
2679
|
[ -f "$_LOOP_MUTE_FILE" ] && return 0
|
|
2676
2680
|
local attach_cmd="${_SHARED_ROOT}/loop/attach-${session}.command"
|
|
2677
|
-
|
|
2681
|
+
# Drop `exec` so the wrapping shell survives `tmux attach` exiting; pause
|
|
2682
|
+
# on `read` afterwards so the user can scroll back through the session's
|
|
2683
|
+
# output before closing the Terminal window. Without this the window
|
|
2684
|
+
# closes the instant the tmux session ends and the entire scrollback
|
|
2685
|
+
# disappears with it.
|
|
2686
|
+
printf '#!/bin/bash\ntmux attach -t %s\necho\necho "================================================================"\necho " session ended. press enter to close this window."\necho "================================================================"\nread _\n' \
|
|
2687
|
+
"$session" > "$attach_cmd" 2>/dev/null || return 0
|
|
2678
2688
|
chmod +x "$attach_cmd" 2>/dev/null || return 0
|
|
2679
2689
|
open -g -a Terminal "$attach_cmd" >/dev/null 2>&1 || true
|
|
2680
2690
|
}
|
|
@@ -2715,14 +2725,14 @@ _peer_call() {
|
|
|
2715
2725
|
local call_timeout
|
|
2716
2726
|
call_timeout="$(config_get "peer_call_timeout" "180")"
|
|
2717
2727
|
|
|
2718
|
-
info "
|
|
2728
|
+
info "$(msg status.peer_call_timeout_s_peer ${call_timeout})"
|
|
2719
2729
|
|
|
2720
2730
|
if [[ -n "$session" ]] && command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session" 2>/dev/null; then
|
|
2721
2731
|
local out_file
|
|
2722
2732
|
out_file=$(mktemp)
|
|
2723
2733
|
local cmd_str
|
|
2724
2734
|
cmd_str=$(_agent_cmd_str "$to" peer "$prompt") || {
|
|
2725
|
-
err "
|
|
2735
|
+
err "$(msg status.unsupported_peer $to)"
|
|
2726
2736
|
return 1
|
|
2727
2737
|
}
|
|
2728
2738
|
_peer_dispatch_in_tmux "$session" "$cmd_str" "$out_file" "$stderr_log" "$call_timeout"
|
|
@@ -2730,7 +2740,7 @@ _peer_call() {
|
|
|
2730
2740
|
rm -f "$out_file"
|
|
2731
2741
|
else
|
|
2732
2742
|
_agent_argv "$to" peer "$prompt" || {
|
|
2733
|
-
err "
|
|
2743
|
+
err "$(msg status.unsupported_peer_2 $to)"
|
|
2734
2744
|
return 1
|
|
2735
2745
|
}
|
|
2736
2746
|
output="$("${_AGENT_ARGV[@]}" 2>"$stderr_log" || true)"
|
|
@@ -2796,7 +2806,7 @@ cmd_peer() {
|
|
|
2796
2806
|
status) subcmd="status"; shift ;;
|
|
2797
2807
|
reset) subcmd="reset"; shift; break ;;
|
|
2798
2808
|
help|--help|-h) subcmd="help"; shift ;;
|
|
2799
|
-
*) err "
|
|
2809
|
+
*) err "$(msg peer.unknown_option_1)"; exit 1 ;;
|
|
2800
2810
|
esac
|
|
2801
2811
|
done
|
|
2802
2812
|
|
|
@@ -2807,7 +2817,7 @@ cmd_peer() {
|
|
|
2807
2817
|
esac
|
|
2808
2818
|
|
|
2809
2819
|
if [[ -z "$from_tool" ]]; then
|
|
2810
|
-
err "
|
|
2820
|
+
err "$(msg peer.from_is_required)"
|
|
2811
2821
|
echo ""
|
|
2812
2822
|
cmd_peer_help
|
|
2813
2823
|
exit 1
|
|
@@ -2816,13 +2826,13 @@ cmd_peer() {
|
|
|
2816
2826
|
if [[ -z "$to_tool" ]]; then
|
|
2817
2827
|
to_tool="$(_peer_route "$from_tool" "$tag")"
|
|
2818
2828
|
if [[ -z "$to_tool" ]]; then
|
|
2819
|
-
err "
|
|
2829
|
+
err "$(msg peer.no_available_peer_found_for_tag $tag)"
|
|
2820
2830
|
echo ""
|
|
2821
2831
|
info "Installed peers: $(_peer_detect_peers)"
|
|
2822
2832
|
info "Capability map: $(config_get "peer_capability_map_${tag}" "$(config_get "peer_capability_map_default" "kimi claude pi")")"
|
|
2823
2833
|
exit 1
|
|
2824
2834
|
fi
|
|
2825
|
-
info "
|
|
2835
|
+
info "$(msg peer.auto_selected_peer $to_tool)"
|
|
2826
2836
|
fi
|
|
2827
2837
|
|
|
2828
2838
|
local pair
|
|
@@ -2831,7 +2841,7 @@ cmd_peer() {
|
|
|
2831
2841
|
local status
|
|
2832
2842
|
status="$(_peer_get_state "$pair" "status")"
|
|
2833
2843
|
if [[ "$status" == "abandoned" ]]; then
|
|
2834
|
-
err "
|
|
2844
|
+
err "$(msg peer.peer_pair_is_abandoned_run_roll $pair $from_tool $to_tool)"
|
|
2835
2845
|
exit 1
|
|
2836
2846
|
fi
|
|
2837
2847
|
|
|
@@ -2840,13 +2850,13 @@ cmd_peer() {
|
|
|
2840
2850
|
opt_out="$(config_get "peer_opt_out_seconds" "10")"
|
|
2841
2851
|
info "Launching peer review: $from_tool → $to_tool (round $round, tag: $tag)"
|
|
2842
2852
|
info "Press Enter to proceed or type 'n' to abort. Auto-executing in ${opt_out}s..."
|
|
2843
|
-
info "
|
|
2844
|
-
info "
|
|
2853
|
+
info "$(msg peer.en_peer_review $from_tool $to_tool $round $tag)"
|
|
2854
|
+
info "$(msg peer.en_enter_n ${opt_out})"
|
|
2845
2855
|
|
|
2846
2856
|
local answer=""
|
|
2847
2857
|
if IFS= read -r -t "$opt_out" answer 2>/dev/null; then
|
|
2848
2858
|
if [[ "$answer" == "n" || "$answer" == "N" ]]; then
|
|
2849
|
-
info "
|
|
2859
|
+
info "$(msg peer.peer_review_aborted_by_user)"
|
|
2850
2860
|
exit 0
|
|
2851
2861
|
fi
|
|
2852
2862
|
fi
|
|
@@ -2893,7 +2903,7 @@ cmd_peer() {
|
|
|
2893
2903
|
echo ""
|
|
2894
2904
|
} > "$log_file"
|
|
2895
2905
|
|
|
2896
|
-
info "
|
|
2906
|
+
info "$(msg peer.calling $to_tool)"
|
|
2897
2907
|
local response
|
|
2898
2908
|
response="$(_peer_call "$to_tool" "$prompt" "$peer_session")"
|
|
2899
2909
|
|
|
@@ -2901,7 +2911,7 @@ cmd_peer() {
|
|
|
2901
2911
|
stderr_log="${_PEER_STATE_DIR}/logs/.last_stderr.log"
|
|
2902
2912
|
if [[ -f "$stderr_log" && -s "$stderr_log" ]]; then
|
|
2903
2913
|
echo ""
|
|
2904
|
-
echo -e "${BOLD}
|
|
2914
|
+
echo -e "$(msg peer.peer_stderr_peer ${BOLD} ${NC})"
|
|
2905
2915
|
cat "$stderr_log"
|
|
2906
2916
|
echo ""
|
|
2907
2917
|
fi
|
|
@@ -2912,14 +2922,14 @@ cmd_peer() {
|
|
|
2912
2922
|
resolution="$(_peer_parse_resolution "$response")"
|
|
2913
2923
|
|
|
2914
2924
|
if [[ -z "$resolution" ]]; then
|
|
2915
|
-
warn "
|
|
2925
|
+
warn "$(msg peer.could_not_parse_resolution_from_peer)"
|
|
2916
2926
|
resolution="UNKNOWN"
|
|
2917
2927
|
fi
|
|
2918
2928
|
|
|
2919
2929
|
_peer_update_state "$pair" "$resolution"
|
|
2920
2930
|
|
|
2921
2931
|
echo ""
|
|
2922
|
-
echo -e "${BOLD}
|
|
2932
|
+
echo -e "$(msg peer.peer_review_result_peer_review ${BOLD} ${NC})"
|
|
2923
2933
|
echo " Pair: $pair"
|
|
2924
2934
|
echo " Round: $round"
|
|
2925
2935
|
echo " Resolution: $resolution"
|
|
@@ -2927,17 +2937,17 @@ cmd_peer() {
|
|
|
2927
2937
|
|
|
2928
2938
|
case "$resolution" in
|
|
2929
2939
|
AGREE)
|
|
2930
|
-
ok "
|
|
2940
|
+
ok "$(msg peer.consensus_reached_proceed_with_execution)"
|
|
2931
2941
|
;;
|
|
2932
2942
|
REFINE|OBJECT)
|
|
2933
2943
|
if [[ "$round" -ge 3 ]]; then
|
|
2934
|
-
warn "
|
|
2944
|
+
warn "$(msg peer.max_rounds_reached_escalating_to_user)"
|
|
2935
2945
|
else
|
|
2936
|
-
info "
|
|
2946
|
+
info "$(msg peer.peer_requests_continue_to_round_round "${resolution}" "$((round + 1))")"
|
|
2937
2947
|
fi
|
|
2938
2948
|
;;
|
|
2939
2949
|
ESCALATE|UNKNOWN)
|
|
2940
|
-
warn "
|
|
2950
|
+
warn "$(msg peer.peer_review_escalated_or_failed_human)"
|
|
2941
2951
|
;;
|
|
2942
2952
|
esac
|
|
2943
2953
|
|
|
@@ -2963,7 +2973,7 @@ cmd_peer() {
|
|
|
2963
2973
|
|
|
2964
2974
|
cmd_peer_status() {
|
|
2965
2975
|
_peer_ensure_state_dir
|
|
2966
|
-
echo -e "${BOLD}
|
|
2976
|
+
echo -e "$(msg peer_status.peer_review_status_peer_review ${BOLD} ${NC})"
|
|
2967
2977
|
echo ""
|
|
2968
2978
|
|
|
2969
2979
|
local found=0
|
|
@@ -2990,7 +3000,7 @@ cmd_peer_status() {
|
|
|
2990
3000
|
done
|
|
2991
3001
|
|
|
2992
3002
|
if [[ "$found" -eq 0 ]]; then
|
|
2993
|
-
info "
|
|
3003
|
+
info "$(msg peer_status.no_peer_review_history_yet)"
|
|
2994
3004
|
fi
|
|
2995
3005
|
|
|
2996
3006
|
echo ""
|
|
@@ -3021,12 +3031,12 @@ cmd_peer_reset() {
|
|
|
3021
3031
|
rm -f "$_PEER_STATE_DIR"/*_streak
|
|
3022
3032
|
rm -f "$_PEER_STATE_DIR"/*_last_outcome
|
|
3023
3033
|
rm -f "$_PEER_STATE_DIR"/*_last_time
|
|
3024
|
-
ok "
|
|
3034
|
+
ok "$(msg peer_reset.all_peer_states_reset)"
|
|
3025
3035
|
return
|
|
3026
3036
|
fi
|
|
3027
3037
|
|
|
3028
3038
|
if [[ -z "$target_pair" ]]; then
|
|
3029
|
-
err "
|
|
3039
|
+
err "$(msg peer_reset.usage_roll_peer_reset_from_to)"
|
|
3030
3040
|
exit 1
|
|
3031
3041
|
fi
|
|
3032
3042
|
|
|
@@ -3034,26 +3044,26 @@ cmd_peer_reset() {
|
|
|
3034
3044
|
rm -f "$(_peer_state_file "$target_pair" "streak")"
|
|
3035
3045
|
rm -f "$(_peer_state_file "$target_pair" "last_outcome")"
|
|
3036
3046
|
rm -f "$(_peer_state_file "$target_pair" "last_time")"
|
|
3037
|
-
ok "
|
|
3047
|
+
ok "$(msg peer_reset.peer_state_reset_peer $target_pair)"
|
|
3038
3048
|
}
|
|
3039
3049
|
|
|
3040
3050
|
cmd_peer_help() {
|
|
3041
3051
|
echo -e "${BOLD}roll peer — Cross-Agent Peer Review${NC}"
|
|
3042
3052
|
echo ""
|
|
3043
|
-
echo "
|
|
3053
|
+
echo "$(msg peer_help.usage_roll_peer_options)"
|
|
3044
3054
|
echo ""
|
|
3045
3055
|
echo "Options:"
|
|
3046
|
-
echo "
|
|
3047
|
-
echo "
|
|
3048
|
-
echo "
|
|
3049
|
-
echo "
|
|
3050
|
-
echo "
|
|
3051
|
-
echo "
|
|
3056
|
+
echo "$(msg peer_help.from_tool_originating_agent_kimi_claude)"
|
|
3057
|
+
echo "$(msg peer_help.to_tool_target_peer_auto_detected)"
|
|
3058
|
+
echo "$(msg peer_help.round_n_current_round_default_1)"
|
|
3059
|
+
echo "$(msg peer_help.tag_type_task_type_for_routing)"
|
|
3060
|
+
echo "$(msg peer_help.context_file_context_file_to_send)"
|
|
3061
|
+
echo "$(msg peer_help.yes_yolo_skip_opt_out_prompt)"
|
|
3052
3062
|
echo ""
|
|
3053
3063
|
echo "Subcommands:"
|
|
3054
|
-
echo "
|
|
3055
|
-
echo "
|
|
3056
|
-
echo "
|
|
3064
|
+
echo "$(msg peer_help.status_show_peer_review_state)"
|
|
3065
|
+
echo "$(msg peer_help.reset_pair_all_reset_peer_state)"
|
|
3066
|
+
echo "$(msg peer_help.help_show_this_help)"
|
|
3057
3067
|
}
|
|
3058
3068
|
|
|
3059
3069
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -3084,18 +3094,6 @@ _project_agent() {
|
|
|
3084
3094
|
fi
|
|
3085
3095
|
}
|
|
3086
3096
|
|
|
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
|
-
|
|
3099
3097
|
_skill_content() {
|
|
3100
3098
|
# Strip YAML frontmatter (---...---) — it's roll-internal metadata, not agent instructions
|
|
3101
3099
|
awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' "$1"
|
|
@@ -3349,12 +3347,19 @@ USAGE 用法
|
|
|
3349
3347
|
在浏览器中打开已渲染的幻灯片
|
|
3350
3348
|
roll slides logs <slug> Show the last build failure log for a deck
|
|
3351
3349
|
显示幻灯片上次构建失败日志
|
|
3350
|
+
roll slides templates List available slide templates (built-in + project)
|
|
3351
|
+
列出可用模板(内置 + 项目自定义)
|
|
3352
|
+
roll slides delete <slug> [--force]
|
|
3353
|
+
Delete a deck (dir + HTML) with confirmation prompt
|
|
3354
|
+
删除幻灯片(含目录与 HTML),需确认
|
|
3352
3355
|
|
|
3353
3356
|
OPTIONS 选项
|
|
3354
3357
|
--no-open Skip auto-opening the rendered HTML in a browser
|
|
3355
3358
|
渲染后不自动打开浏览器
|
|
3356
3359
|
--no-build Skip auto-build after agent completes (deck.md only)
|
|
3357
3360
|
仅生成 deck.md,不自动渲染
|
|
3361
|
+
--force Skip confirmation prompt (delete subcommand)
|
|
3362
|
+
跳过确认提示(delete 子命令)
|
|
3358
3363
|
--help, -h Show this help
|
|
3359
3364
|
显示本帮助
|
|
3360
3365
|
EOF
|
|
@@ -3433,12 +3438,12 @@ cmd_slides_build() {
|
|
|
3433
3438
|
case "$1" in
|
|
3434
3439
|
--no-open) no_open=1; shift ;;
|
|
3435
3440
|
--help|-h) _slides_help; return 0 ;;
|
|
3436
|
-
--*) err "
|
|
3441
|
+
--*) err "$(msg slides_build.unknown_option_1)"; return 1 ;;
|
|
3437
3442
|
*)
|
|
3438
3443
|
if [[ -z "$slug" ]]; then
|
|
3439
3444
|
slug="$1"; shift
|
|
3440
3445
|
else
|
|
3441
|
-
err "
|
|
3446
|
+
err "$(msg slides_build.unexpected_argument_1)"; return 1
|
|
3442
3447
|
fi
|
|
3443
3448
|
;;
|
|
3444
3449
|
esac
|
|
@@ -3446,16 +3451,16 @@ cmd_slides_build() {
|
|
|
3446
3451
|
|
|
3447
3452
|
if [[ -z "$slug" ]]; then
|
|
3448
3453
|
err "Usage: roll slides build <slug> [--no-open]"
|
|
3449
|
-
echo "
|
|
3454
|
+
echo "$(msg slides_build.usage_roll_slides_build_slug_no)" >&2
|
|
3450
3455
|
return 1
|
|
3451
3456
|
fi
|
|
3452
3457
|
|
|
3453
3458
|
local deck=".roll/slides/${slug}/deck.md"
|
|
3454
3459
|
if [[ ! -f "$deck" ]]; then
|
|
3455
3460
|
err "Deck not found: ${deck}"
|
|
3456
|
-
echo "
|
|
3461
|
+
echo "$(msg slides_build.en_deck ${deck})" >&2
|
|
3457
3462
|
echo " Hint: run 'roll slides new \"<topic>\"' to generate a new deck." >&2
|
|
3458
|
-
echo "
|
|
3463
|
+
echo "$(msg slides_build.en_roll_slides_new)" >&2
|
|
3459
3464
|
return 1
|
|
3460
3465
|
fi
|
|
3461
3466
|
|
|
@@ -3463,7 +3468,7 @@ cmd_slides_build() {
|
|
|
3463
3468
|
local validator="${lib_dir}/slides-validate.py"
|
|
3464
3469
|
local renderer="${lib_dir}/slides-render.py"
|
|
3465
3470
|
if [[ ! -f "$validator" || ! -f "$renderer" ]]; then
|
|
3466
|
-
err "
|
|
3471
|
+
err "$(msg slides_build.slides_toolchain_missing_re_run_roll)"
|
|
3467
3472
|
return 1
|
|
3468
3473
|
fi
|
|
3469
3474
|
|
|
@@ -3480,8 +3485,10 @@ cmd_slides_build() {
|
|
|
3480
3485
|
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3481
3486
|
mkdir -p ".roll/slides/${slug}"
|
|
3482
3487
|
printf '[%s] stage=validate\n%s\n' "$ts" "$val_out" > "$err_file"
|
|
3483
|
-
|
|
3484
|
-
|
|
3488
|
+
# Print validator output so user can see what failed.
|
|
3489
|
+
printf '%s\n' "$val_out" >&2
|
|
3490
|
+
echo -e "${RED}[FAIL]${NC} $(msg slides_build.validation_failed_for "${deck}")" >&2
|
|
3491
|
+
echo " $(msg slides_build.hint_fix_and_rerun "${deck}" "${slug}")" >&2
|
|
3485
3492
|
return 1
|
|
3486
3493
|
fi
|
|
3487
3494
|
|
|
@@ -3492,7 +3499,26 @@ cmd_slides_build() {
|
|
|
3492
3499
|
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3493
3500
|
mkdir -p ".roll/slides/${slug}"
|
|
3494
3501
|
printf '[%s] stage=template\ntemplate not found: %s\n' "$ts" "$tpl_name" > "$err_file"
|
|
3495
|
-
|
|
3502
|
+
echo -e "${RED}[FAIL]${NC} $(msg slides_build.template_not_found "${tpl_name}")" >&2
|
|
3503
|
+
# List available templates (built-in + project overrides).
|
|
3504
|
+
echo " $(msg slides_build.available_templates)" >&2
|
|
3505
|
+
local builtin_dir="${ROLL_PKG_DIR}/lib/slides/templates"
|
|
3506
|
+
if [[ -d "$builtin_dir" ]]; then
|
|
3507
|
+
local t
|
|
3508
|
+
for t in "$builtin_dir"/*.html; do
|
|
3509
|
+
local n="${t##*/}"; n="${n%.html}"
|
|
3510
|
+
printf ' %-20s (builtin)\n' "$n" >&2
|
|
3511
|
+
done
|
|
3512
|
+
fi
|
|
3513
|
+
local proj_dir=".roll/slides/templates"
|
|
3514
|
+
if [[ -d "$proj_dir" ]]; then
|
|
3515
|
+
local t
|
|
3516
|
+
for t in "$proj_dir"/*.html; do
|
|
3517
|
+
local n="${t##*/}"; n="${n%.html}"
|
|
3518
|
+
printf ' %-20s (project)\n' "$n" >&2
|
|
3519
|
+
done
|
|
3520
|
+
fi
|
|
3521
|
+
echo " $(msg slides_build.templates_list_hint)" >&2
|
|
3496
3522
|
return 1
|
|
3497
3523
|
fi
|
|
3498
3524
|
|
|
@@ -3502,7 +3528,13 @@ cmd_slides_build() {
|
|
|
3502
3528
|
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3503
3529
|
mkdir -p ".roll/slides/${slug}"
|
|
3504
3530
|
printf '[%s] stage=render\n%s\n' "$ts" "$render_out" > "$err_file"
|
|
3505
|
-
|
|
3531
|
+
echo -e "${RED}[FAIL]${NC} $(msg slides_build.renderer_crashed_for "${deck}")" >&2
|
|
3532
|
+
echo " $(msg slides_build.see_full_error_logs "${slug}")" >&2
|
|
3533
|
+
local last_lines; last_lines=$(printf '%s' "$render_out" | tail -n 5)
|
|
3534
|
+
if [[ -n "$last_lines" ]]; then
|
|
3535
|
+
echo " $(msg slides_build.last_5_lines_of_renderer_output)" >&2
|
|
3536
|
+
printf '%s\n' "$last_lines" >&2
|
|
3537
|
+
fi
|
|
3506
3538
|
return 1
|
|
3507
3539
|
}
|
|
3508
3540
|
|
|
@@ -3512,7 +3544,7 @@ cmd_slides_build() {
|
|
|
3512
3544
|
# 3. Default-ignore the HTML artefact so it doesn't accidentally get committed.
|
|
3513
3545
|
_slides_ensure_gitignore
|
|
3514
3546
|
|
|
3515
|
-
ok "
|
|
3547
|
+
ok "$(msg slides_build.rendered ${out})"
|
|
3516
3548
|
|
|
3517
3549
|
# 4. Auto-open browser unless suppressed (or running inside bats tests).
|
|
3518
3550
|
if [[ "$no_open" -eq 1 ]] || [[ -n "${BATS_TEST_NUMBER:-}" ]] || [[ -n "${ROLL_SLIDES_NO_OPEN:-}" ]]; then
|
|
@@ -3563,16 +3595,16 @@ cmd_slides_list() {
|
|
|
3563
3595
|
while [[ $# -gt 0 ]]; do
|
|
3564
3596
|
case "$1" in
|
|
3565
3597
|
--help|-h) _slides_help; return 0 ;;
|
|
3566
|
-
--*) err "
|
|
3567
|
-
*) err "
|
|
3598
|
+
--*) err "$(msg slides_list.unknown_option_1)"; return 1 ;;
|
|
3599
|
+
*) err "$(msg slides_list.unexpected_argument_1)"; return 1 ;;
|
|
3568
3600
|
esac
|
|
3569
3601
|
done
|
|
3570
3602
|
|
|
3571
3603
|
local slides_dir=".roll/slides"
|
|
3572
3604
|
if [[ ! -d "$slides_dir" ]]; then
|
|
3573
|
-
info "
|
|
3605
|
+
info "$(msg slides_list.no_decks_found_under_roll_slides)"
|
|
3574
3606
|
echo " Hint: run 'roll slides new \"<topic>\"' to create one."
|
|
3575
|
-
echo "
|
|
3607
|
+
echo "$(msg slides_list.en_roll_slides_new)"
|
|
3576
3608
|
return 0
|
|
3577
3609
|
fi
|
|
3578
3610
|
|
|
@@ -3589,9 +3621,9 @@ cmd_slides_list() {
|
|
|
3589
3621
|
shopt -u nullglob
|
|
3590
3622
|
|
|
3591
3623
|
if [[ "${#slugs[@]}" -eq 0 ]]; then
|
|
3592
|
-
info "
|
|
3624
|
+
info "$(msg slides_list.no_decks_found_under_roll_slides_2)"
|
|
3593
3625
|
echo " Hint: run 'roll slides new \"<topic>\"' to create one."
|
|
3594
|
-
echo "
|
|
3626
|
+
echo "$(msg slides_list.en_roll_slides_new_2)"
|
|
3595
3627
|
return 0
|
|
3596
3628
|
fi
|
|
3597
3629
|
|
|
@@ -3646,12 +3678,12 @@ cmd_slides_preview() {
|
|
|
3646
3678
|
case "$1" in
|
|
3647
3679
|
--no-open) no_open=1; shift ;;
|
|
3648
3680
|
--help|-h) _slides_help; return 0 ;;
|
|
3649
|
-
--*) err "
|
|
3681
|
+
--*) err "$(msg slides_preview.unknown_option_1)"; return 1 ;;
|
|
3650
3682
|
*)
|
|
3651
3683
|
if [[ -z "$slug" ]]; then
|
|
3652
3684
|
slug="$1"; shift
|
|
3653
3685
|
else
|
|
3654
|
-
err "
|
|
3686
|
+
err "$(msg slides_preview.unexpected_argument_1)"; return 1
|
|
3655
3687
|
fi
|
|
3656
3688
|
;;
|
|
3657
3689
|
esac
|
|
@@ -3659,20 +3691,20 @@ cmd_slides_preview() {
|
|
|
3659
3691
|
|
|
3660
3692
|
if [[ -z "$slug" ]]; then
|
|
3661
3693
|
err "Usage: roll slides preview <slug> [--no-open]"
|
|
3662
|
-
echo "
|
|
3694
|
+
echo "$(msg slides_preview.usage_roll_slides_preview_slug_no)" >&2
|
|
3663
3695
|
return 1
|
|
3664
3696
|
fi
|
|
3665
3697
|
|
|
3666
3698
|
local html=".roll/slides/${slug}.html"
|
|
3667
3699
|
if [[ ! -f "$html" ]]; then
|
|
3668
3700
|
err "Rendered HTML not found: ${html}"
|
|
3669
|
-
echo "
|
|
3701
|
+
echo "$(msg slides_preview.en_html ${html})" >&2
|
|
3670
3702
|
echo " Hint: run 'roll slides build ${slug}' first to render it." >&2
|
|
3671
|
-
echo "
|
|
3703
|
+
echo "$(msg slides_preview.en_roll_slides_build ${slug})" >&2
|
|
3672
3704
|
return 1
|
|
3673
3705
|
fi
|
|
3674
3706
|
|
|
3675
|
-
ok "
|
|
3707
|
+
ok "$(msg slides_preview.preview ${html})"
|
|
3676
3708
|
|
|
3677
3709
|
if [[ "$no_open" -eq 1 ]] || [[ -n "${BATS_TEST_NUMBER:-}" ]] || [[ -n "${ROLL_SLIDES_NO_OPEN:-}" ]]; then
|
|
3678
3710
|
return 0
|
|
@@ -3690,12 +3722,12 @@ cmd_slides_logs() {
|
|
|
3690
3722
|
while [[ $# -gt 0 ]]; do
|
|
3691
3723
|
case "$1" in
|
|
3692
3724
|
--help|-h) _slides_help; return 0 ;;
|
|
3693
|
-
--*) err "
|
|
3725
|
+
--*) err "$(msg slides_logs.unknown_option_1)"; return 1 ;;
|
|
3694
3726
|
*)
|
|
3695
3727
|
if [[ -z "$slug" ]]; then
|
|
3696
3728
|
slug="$1"; shift
|
|
3697
3729
|
else
|
|
3698
|
-
err "
|
|
3730
|
+
err "$(msg slides_logs.unexpected_argument_1)"; return 1
|
|
3699
3731
|
fi
|
|
3700
3732
|
;;
|
|
3701
3733
|
esac
|
|
@@ -3703,7 +3735,7 @@ cmd_slides_logs() {
|
|
|
3703
3735
|
|
|
3704
3736
|
if [[ -z "$slug" ]]; then
|
|
3705
3737
|
err "Usage: roll slides logs <slug>"
|
|
3706
|
-
echo "
|
|
3738
|
+
echo "$(msg slides_logs.usage_roll_slides_logs_slug)" >&2
|
|
3707
3739
|
return 1
|
|
3708
3740
|
fi
|
|
3709
3741
|
|
|
@@ -3711,12 +3743,12 @@ cmd_slides_logs() {
|
|
|
3711
3743
|
local err_file="${deck_dir}/.last-build.err"
|
|
3712
3744
|
|
|
3713
3745
|
if [[ ! -d "$deck_dir" ]] || [[ ! -f "${deck_dir}/deck.md" ]]; then
|
|
3714
|
-
err "
|
|
3746
|
+
err "$(msg slides_logs.deck_not_found ${slug})"
|
|
3715
3747
|
return 1
|
|
3716
3748
|
fi
|
|
3717
3749
|
|
|
3718
3750
|
if [[ ! -f "$err_file" ]]; then
|
|
3719
|
-
info "
|
|
3751
|
+
info "$(msg slides_logs.no_failure_records_for ${slug})"
|
|
3720
3752
|
return 0
|
|
3721
3753
|
fi
|
|
3722
3754
|
|
|
@@ -3724,6 +3756,114 @@ cmd_slides_logs() {
|
|
|
3724
3756
|
return 0
|
|
3725
3757
|
}
|
|
3726
3758
|
|
|
3759
|
+
# ─── US-DECK-014 ─────────────────────────────────────────────────────────────
|
|
3760
|
+
cmd_slides_delete() {
|
|
3761
|
+
local slug="" force=0
|
|
3762
|
+
while [[ $# -gt 0 ]]; do
|
|
3763
|
+
case "$1" in
|
|
3764
|
+
--force) force=1; shift ;;
|
|
3765
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3766
|
+
--*) err "$(msg slides_delete.unknown_option_1)"; return 1 ;;
|
|
3767
|
+
*)
|
|
3768
|
+
if [[ -z "$slug" ]]; then
|
|
3769
|
+
slug="$1"; shift
|
|
3770
|
+
else
|
|
3771
|
+
err "$(msg slides_delete.unexpected_argument_1)"; return 1
|
|
3772
|
+
fi
|
|
3773
|
+
;;
|
|
3774
|
+
esac
|
|
3775
|
+
done
|
|
3776
|
+
|
|
3777
|
+
if [[ -z "$slug" ]]; then
|
|
3778
|
+
err "Usage: roll slides delete <slug> [--force]"
|
|
3779
|
+
echo "$(msg slides_delete.usage_roll_slides_delete_slug_force)" >&2
|
|
3780
|
+
return 1
|
|
3781
|
+
fi
|
|
3782
|
+
|
|
3783
|
+
local deck_dir=".roll/slides/${slug}"
|
|
3784
|
+
local html=".roll/slides/${slug}.html"
|
|
3785
|
+
|
|
3786
|
+
if [[ ! -d "$deck_dir" ]] || [[ ! -f "${deck_dir}/deck.md" ]]; then
|
|
3787
|
+
err "$(msg slides_delete.deck_not_found ${slug})"
|
|
3788
|
+
return 1
|
|
3789
|
+
fi
|
|
3790
|
+
|
|
3791
|
+
# Non-TTY must use --force (skip interactive confirmation)
|
|
3792
|
+
if [[ $force -eq 0 ]]; then
|
|
3793
|
+
if [[ ! -t 0 ]]; then
|
|
3794
|
+
err "$(msg slides_delete.non_interactive_terminal_must_use_force)"
|
|
3795
|
+
return 1
|
|
3796
|
+
fi
|
|
3797
|
+
printf '%s ' "$(msg slides_delete.prompt "$slug")" >&2
|
|
3798
|
+
read -r answer
|
|
3799
|
+
case "$answer" in
|
|
3800
|
+
[yY]|[yY][eE][sS]) : ;;
|
|
3801
|
+
*) info "$(msg slides_delete.cancelled)"; return 0 ;;
|
|
3802
|
+
esac
|
|
3803
|
+
fi
|
|
3804
|
+
|
|
3805
|
+
# Remove deck directory and HTML file
|
|
3806
|
+
rm -rf "$deck_dir" 2>/dev/null || true
|
|
3807
|
+
rm -f "$html" 2>/dev/null || true
|
|
3808
|
+
ok "$(msg slides_delete.deleted ${slug})"
|
|
3809
|
+
return 0
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
# ─── US-DECK-014 ─────────────────────────────────────────────────────────────
|
|
3813
|
+
cmd_slides_templates() {
|
|
3814
|
+
while [[ $# -gt 0 ]]; do
|
|
3815
|
+
case "$1" in
|
|
3816
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3817
|
+
--*) err "$(msg slides_templates.unknown_option_1)"; return 1 ;;
|
|
3818
|
+
*) err "$(msg slides_templates.unexpected_argument_1)"; return 1 ;;
|
|
3819
|
+
esac
|
|
3820
|
+
done
|
|
3821
|
+
|
|
3822
|
+
local seen=""
|
|
3823
|
+
local found=0
|
|
3824
|
+
local name base path source
|
|
3825
|
+
|
|
3826
|
+
printf '%-24s %-12s %s\n' "name" "source" "path"
|
|
3827
|
+
printf '%-24s %-12s %s\n' "----" "------" "----"
|
|
3828
|
+
|
|
3829
|
+
# Built-in templates (shipped with roll package)
|
|
3830
|
+
local builtin_dir="${ROLL_PKG_DIR}/lib/slides/templates"
|
|
3831
|
+
if [[ -d "$builtin_dir" ]]; then
|
|
3832
|
+
shopt -s nullglob
|
|
3833
|
+
for tpl in "$builtin_dir"/*.html; do
|
|
3834
|
+
name="${tpl##*/}"
|
|
3835
|
+
name="${name%.html}"
|
|
3836
|
+
printf '%-24s %-12s %s\n' "$name" "builtin" "$tpl"
|
|
3837
|
+
found=1
|
|
3838
|
+
done
|
|
3839
|
+
shopt -u nullglob
|
|
3840
|
+
fi
|
|
3841
|
+
|
|
3842
|
+
# Project-level overrides (.roll/slides/templates/)
|
|
3843
|
+
local proj_dir=".roll/slides/templates"
|
|
3844
|
+
if [[ -d "$proj_dir" ]]; then
|
|
3845
|
+
shopt -s nullglob
|
|
3846
|
+
for tpl in "$proj_dir"/*.html; do
|
|
3847
|
+
name="${tpl##*/}"
|
|
3848
|
+
name="${name%.html}"
|
|
3849
|
+
# Mark as project override if same name exists in builtin
|
|
3850
|
+
if [[ -f "${builtin_dir}/${name}.html" ]]; then
|
|
3851
|
+
source="project (override)"
|
|
3852
|
+
else
|
|
3853
|
+
source="project"
|
|
3854
|
+
fi
|
|
3855
|
+
printf '%-24s %-12s %s\n' "$name" "$source" "$tpl"
|
|
3856
|
+
found=1
|
|
3857
|
+
done
|
|
3858
|
+
shopt -u nullglob
|
|
3859
|
+
fi
|
|
3860
|
+
|
|
3861
|
+
if [[ $found -eq 0 ]]; then
|
|
3862
|
+
info "$(msg slides_templates.no_templates_found)"
|
|
3863
|
+
fi
|
|
3864
|
+
return 0
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3727
3867
|
# ─── US-DECK-004 ─────────────────────────────────────────────────────────────
|
|
3728
3868
|
# Turn a topic string into a kebab-case slug.
|
|
3729
3869
|
# Lower-cases, replaces any run of non-alphanumerics with a single dash,
|
|
@@ -3750,18 +3890,18 @@ cmd_slides_new() {
|
|
|
3750
3890
|
while [[ $# -gt 0 ]]; do
|
|
3751
3891
|
case "$1" in
|
|
3752
3892
|
--template)
|
|
3753
|
-
[[ -n "${2:-}" ]] || { err "
|
|
3893
|
+
[[ -n "${2:-}" ]] || { err "$(msg slides_new.template_requires_value)"; return 1; }
|
|
3754
3894
|
template="$2"; shift 2 ;;
|
|
3755
3895
|
--template=*) template="${1#--template=}"; shift ;;
|
|
3756
3896
|
--quiet) quiet=1; shift ;;
|
|
3757
3897
|
--no-build) no_build=1; shift ;;
|
|
3758
3898
|
--help|-h) _slides_help; return 0 ;;
|
|
3759
|
-
--*) err "
|
|
3899
|
+
--*) err "$(msg slides_new.unknown_option_1)"; return 1 ;;
|
|
3760
3900
|
*)
|
|
3761
3901
|
if [[ -z "$topic" ]]; then
|
|
3762
3902
|
topic="$1"; shift
|
|
3763
3903
|
else
|
|
3764
|
-
err "
|
|
3904
|
+
err "$(msg slides_new.unexpected_argument_1)"; return 1
|
|
3765
3905
|
fi
|
|
3766
3906
|
;;
|
|
3767
3907
|
esac
|
|
@@ -3769,14 +3909,14 @@ cmd_slides_new() {
|
|
|
3769
3909
|
|
|
3770
3910
|
if [[ -z "$topic" ]]; then
|
|
3771
3911
|
err "Usage: roll slides new \"<topic>\" [--template <name>] [--quiet] [--no-build]"
|
|
3772
|
-
echo "
|
|
3912
|
+
echo "$(msg slides_new.en_roll_slides_new_template)" >&2
|
|
3773
3913
|
return 1
|
|
3774
3914
|
fi
|
|
3775
3915
|
|
|
3776
3916
|
local slug; slug=$(_slides_topic_slug "$topic")
|
|
3777
3917
|
if [[ -z "$slug" ]]; then
|
|
3778
3918
|
err "Could not derive a slug from topic: $topic"
|
|
3779
|
-
echo "
|
|
3919
|
+
echo "$(msg slides_new.en_slug $topic)" >&2
|
|
3780
3920
|
return 1
|
|
3781
3921
|
fi
|
|
3782
3922
|
|
|
@@ -3898,7 +4038,7 @@ EOF
|
|
|
3898
4038
|
# Print Next hint (unless --quiet flag was explicitly passed)
|
|
3899
4039
|
echo
|
|
3900
4040
|
echo "Next: roll slides build ${slug}"
|
|
3901
|
-
echo "
|
|
4041
|
+
echo "$(msg slides_new.en_roll_slides_build ${slug})"
|
|
3902
4042
|
|
|
3903
4043
|
return "$rc"
|
|
3904
4044
|
}
|
|
@@ -3961,7 +4101,7 @@ cmd_prices_refresh() {
|
|
|
3961
4101
|
case "$1" in
|
|
3962
4102
|
--url) url="$2"; shift 2 ;;
|
|
3963
4103
|
--vendor) vendor="$2"; shift 2 ;;
|
|
3964
|
-
*) err "
|
|
4104
|
+
*) err "$(msg prices_refresh.unknown_flag_1)"; return 1 ;;
|
|
3965
4105
|
esac
|
|
3966
4106
|
done
|
|
3967
4107
|
local lib_dir="${ROLL_PKG_DIR}/lib"
|
|
@@ -3985,8 +4125,8 @@ VENDOR_URLS = {
|
|
|
3985
4125
|
if vendor:
|
|
3986
4126
|
default_url = VENDOR_URLS.get(vendor)
|
|
3987
4127
|
if not default_url:
|
|
3988
|
-
print(f"
|
|
3989
|
-
print(f"
|
|
4128
|
+
print(f"$(msg prices_refresh.roll_unknown_vendor_vendor)", file=sys.stderr)
|
|
4129
|
+
print(f"$(msg prices_refresh.roll_known_vendors_join_sorted_vendor)", file=sys.stderr)
|
|
3990
4130
|
sys.exit(1)
|
|
3991
4131
|
url = override_url or default_url
|
|
3992
4132
|
pf.DEFAULT_SOURCE_URL = url
|
|
@@ -4026,7 +4166,7 @@ cmd_prices() {
|
|
|
4026
4166
|
refresh) cmd_prices_refresh "$@" ;;
|
|
4027
4167
|
--help|-h|help|"") _prices_help ;;
|
|
4028
4168
|
*)
|
|
4029
|
-
err "
|
|
4169
|
+
err "$(msg prices.unknown_subcommand ${subcmd})"
|
|
4030
4170
|
_prices_help >&2
|
|
4031
4171
|
return 1
|
|
4032
4172
|
;;
|
|
@@ -4057,7 +4197,7 @@ Read-only — never edits CHANGELOG. Use to catch drift before release.
|
|
|
4057
4197
|
EOF
|
|
4058
4198
|
;;
|
|
4059
4199
|
*)
|
|
4060
|
-
err "
|
|
4200
|
+
err "$(msg changelog.unknown_subcommand ${subcmd})"
|
|
4061
4201
|
err "Try: roll changelog audit"
|
|
4062
4202
|
return 1
|
|
4063
4203
|
;;
|
|
@@ -4083,6 +4223,12 @@ cmd_slides() {
|
|
|
4083
4223
|
logs)
|
|
4084
4224
|
cmd_slides_logs "$@"
|
|
4085
4225
|
;;
|
|
4226
|
+
templates)
|
|
4227
|
+
cmd_slides_templates "$@"
|
|
4228
|
+
;;
|
|
4229
|
+
delete)
|
|
4230
|
+
cmd_slides_delete "$@"
|
|
4231
|
+
;;
|
|
4086
4232
|
--help|-h|help)
|
|
4087
4233
|
_slides_help
|
|
4088
4234
|
return 0
|
|
@@ -4092,7 +4238,7 @@ cmd_slides() {
|
|
|
4092
4238
|
return 1
|
|
4093
4239
|
;;
|
|
4094
4240
|
*)
|
|
4095
|
-
err "
|
|
4241
|
+
err "$(msg slides.unknown_subcommand ${subcmd})"
|
|
4096
4242
|
_slides_help >&2
|
|
4097
4243
|
return 1
|
|
4098
4244
|
;;
|
|
@@ -4201,7 +4347,7 @@ cmd_agent() {
|
|
|
4201
4347
|
use)
|
|
4202
4348
|
local name="${1:-}"
|
|
4203
4349
|
[[ -z "$name" ]] && { err "Usage: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"; exit 1; }
|
|
4204
|
-
command -v "$name" &>/dev/null || warn "$
|
|
4350
|
+
command -v "$name" &>/dev/null || warn "$(msg agent.not_found_in_path_setting_anyway "$name")"
|
|
4205
4351
|
# REFACTOR-040: write to .roll/local.yaml (per-machine state). Migrate
|
|
4206
4352
|
# from legacy .roll.yaml in the project root on the spot — copy the
|
|
4207
4353
|
# value over once, then delete the old file so the root stays clean.
|
|
@@ -4222,17 +4368,17 @@ cmd_agent() {
|
|
|
4222
4368
|
mv "$tmp" .roll.yaml
|
|
4223
4369
|
[[ -s ".roll.yaml" ]] || rm -f .roll.yaml
|
|
4224
4370
|
fi
|
|
4225
|
-
ok "
|
|
4371
|
+
ok "$(msg agent.agent_set_to_for_this_project ${name})"
|
|
4226
4372
|
local project_path; project_path=$(pwd -P)
|
|
4227
4373
|
local slug; slug=$(_project_slug "$project_path")
|
|
4228
4374
|
local runner="${_SHARED_ROOT}/loop/run-${slug}.sh"
|
|
4229
4375
|
if [[ -f "$runner" ]]; then
|
|
4230
4376
|
_install_launchd_plists "$project_path" >/dev/null
|
|
4231
|
-
ok "
|
|
4377
|
+
ok "$(msg agent.loop_runner_scripts_regenerated_for_new)"
|
|
4232
4378
|
fi
|
|
4233
4379
|
;;
|
|
4234
4380
|
list)
|
|
4235
|
-
echo ""; echo "
|
|
4381
|
+
echo ""; echo " $(msg agent.available_agents)"; echo ""
|
|
4236
4382
|
local current; current=$(_project_agent)
|
|
4237
4383
|
for a in claude kimi deepseek opencode codex pi; do
|
|
4238
4384
|
if command -v "$a" &>/dev/null; then
|
|
@@ -4290,6 +4436,47 @@ _project_slug() {
|
|
|
4290
4436
|
if [[ -n "$_common" && "$_common" == *"/.git" ]]; then
|
|
4291
4437
|
path="${_common%/.git}"
|
|
4292
4438
|
fi
|
|
4439
|
+
|
|
4440
|
+
# US-OBS-010: derive slug from git remote URL for stable cross-machine
|
|
4441
|
+
# identity. Normalize: strip .git, git@HOST:PATH → https://HOST/PATH,
|
|
4442
|
+
# lowercase. Fallback chain: origin → first available remote → path-based.
|
|
4443
|
+
local remote_url
|
|
4444
|
+
remote_url=$(git -C "$path" remote get-url origin 2>/dev/null)
|
|
4445
|
+
if [[ -z "$remote_url" ]]; then
|
|
4446
|
+
local first_remote
|
|
4447
|
+
first_remote=$(git -C "$path" remote 2>/dev/null | head -1)
|
|
4448
|
+
if [[ -n "$first_remote" ]]; then
|
|
4449
|
+
remote_url=$(git -C "$path" remote get-url "$first_remote" 2>/dev/null)
|
|
4450
|
+
fi
|
|
4451
|
+
fi
|
|
4452
|
+
|
|
4453
|
+
if [[ -n "$remote_url" ]]; then
|
|
4454
|
+
remote_url="${remote_url%.git}"
|
|
4455
|
+
if [[ "$remote_url" =~ ^git@([^:]+):(.+)$ ]]; then
|
|
4456
|
+
remote_url="https://${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
|
4457
|
+
fi
|
|
4458
|
+
remote_url=$(printf '%s' "$remote_url" | tr '[:upper:]' '[:lower:]')
|
|
4459
|
+
local base; base=$(basename "$remote_url")
|
|
4460
|
+
local hash
|
|
4461
|
+
if command -v md5 &>/dev/null; then
|
|
4462
|
+
hash=$(printf '%s' "$remote_url" | md5 | cut -c1-6)
|
|
4463
|
+
else
|
|
4464
|
+
hash=$(printf '%s' "$remote_url" | md5sum | cut -c1-6)
|
|
4465
|
+
fi
|
|
4466
|
+
base=$(printf '%s' "$base" | tr -cs '[:alnum:]' '-' | sed 's/-*$//')
|
|
4467
|
+
printf '%s' "${base}-${hash}"
|
|
4468
|
+
return 0
|
|
4469
|
+
fi
|
|
4470
|
+
|
|
4471
|
+
# No remote available — fall back to path-based slug.
|
|
4472
|
+
# If roll_records_remote is configured, warn the user: the slug won't be
|
|
4473
|
+
# stable across machines, so cross-machine sync cannot work.
|
|
4474
|
+
local records_remote
|
|
4475
|
+
records_remote=$(config_get "roll_records_remote" "")
|
|
4476
|
+
if [[ -n "$records_remote" ]]; then
|
|
4477
|
+
printf 'roll: WARNING — roll_records_remote is configured but no git remote URL found; slug will fall back to path-based (cross-machine merge will not work)\n' >&2
|
|
4478
|
+
fi
|
|
4479
|
+
|
|
4293
4480
|
local base; base=$(basename "$path")
|
|
4294
4481
|
local hash
|
|
4295
4482
|
if command -v md5 &>/dev/null; then
|
|
@@ -4389,6 +4576,144 @@ PYEOF
|
|
|
4389
4576
|
rm -f "${loop_dir}/run-${old_slug}.sh" "${loop_dir}/run-${old_slug}-inner.sh"
|
|
4390
4577
|
}
|
|
4391
4578
|
|
|
4579
|
+
# US-OBS-010: path-based slug (without remote URL) — used to detect the
|
|
4580
|
+
# pre-remote slug so migration can merge old records into the new identity.
|
|
4581
|
+
_project_slug_path_based() {
|
|
4582
|
+
local path="${1:-$(pwd -P 2>/dev/null || pwd)}"
|
|
4583
|
+
if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
|
|
4584
|
+
local _canon
|
|
4585
|
+
_canon=$(realpath "$path" 2>/dev/null) && path="$_canon"
|
|
4586
|
+
fi
|
|
4587
|
+
local _common
|
|
4588
|
+
_common=$(git -C "$path" rev-parse --git-common-dir 2>/dev/null)
|
|
4589
|
+
if [[ -n "$_common" && "$_common" == *"/.git" ]]; then
|
|
4590
|
+
path="${_common%/.git}"
|
|
4591
|
+
fi
|
|
4592
|
+
local base; base=$(basename "$path")
|
|
4593
|
+
local hash
|
|
4594
|
+
if command -v md5 &>/dev/null; then
|
|
4595
|
+
hash=$(printf '%s' "$path" | md5 | cut -c1-6)
|
|
4596
|
+
else
|
|
4597
|
+
hash=$(printf '%s' "$path" | md5sum | cut -c1-6)
|
|
4598
|
+
fi
|
|
4599
|
+
base=$(printf '%s' "$base" | tr -cs '[:alnum:]' '-' | sed 's/-*$//')
|
|
4600
|
+
printf '%s' "${base}-${hash}"
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
# US-OBS-010: migrate loop records from old path-based slug to new
|
|
4604
|
+
# remote-based slug. Dedup by run_id; atomic cp→tmp→mv; keep old as .bak.
|
|
4605
|
+
#
|
|
4606
|
+
# Usage: _slug_migrate_to_remote <project_path> [<loop_dir>]
|
|
4607
|
+
_slug_migrate_to_remote() {
|
|
4608
|
+
local project_path="$1"
|
|
4609
|
+
local loop_dir="${2:-${_SHARED_ROOT}/loop}"
|
|
4610
|
+
|
|
4611
|
+
local new_slug; new_slug=$(_project_slug "$project_path")
|
|
4612
|
+
local old_slug; old_slug=$(_project_slug_path_based "$project_path")
|
|
4613
|
+
|
|
4614
|
+
# Dedup guard: no migration needed
|
|
4615
|
+
[[ "$old_slug" == "$new_slug" ]] && return 0
|
|
4616
|
+
[[ -f "${loop_dir}/events-${old_slug}.ndjson" ]] || return 0
|
|
4617
|
+
|
|
4618
|
+
printf 'roll: migrating loop records %s → %s (cross-machine slug)\n' "$old_slug" "$new_slug" >&2
|
|
4619
|
+
|
|
4620
|
+
# Migrate events with run_id dedup + atomic write
|
|
4621
|
+
local events_file="${loop_dir}/events-${new_slug}.ndjson"
|
|
4622
|
+
local tmp_events; tmp_events=$(mktemp)
|
|
4623
|
+
|
|
4624
|
+
# Collect existing run_ids from new slug file
|
|
4625
|
+
local existing_ids=""
|
|
4626
|
+
if [[ -f "$events_file" ]]; then
|
|
4627
|
+
existing_ids=$(python3 -c "
|
|
4628
|
+
import json, sys
|
|
4629
|
+
try:
|
|
4630
|
+
with open('$events_file') as f:
|
|
4631
|
+
for line in f:
|
|
4632
|
+
d = json.loads(line.strip())
|
|
4633
|
+
if 'label' in d:
|
|
4634
|
+
print(d['label'])
|
|
4635
|
+
except: pass
|
|
4636
|
+
" 2>/dev/null)
|
|
4637
|
+
fi
|
|
4638
|
+
|
|
4639
|
+
# Append old events to new, dedup by label (run_id / cycle_id)
|
|
4640
|
+
python3 - "$old_slug" "$events_file" "$tmp_events" << 'PYEOF'
|
|
4641
|
+
import json, sys
|
|
4642
|
+
old_slug, new_file, tmp_file = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
4643
|
+
|
|
4644
|
+
# Read existing new-file ids
|
|
4645
|
+
seen = set()
|
|
4646
|
+
try:
|
|
4647
|
+
with open(new_file) as f:
|
|
4648
|
+
for line in f:
|
|
4649
|
+
d = json.loads(line.strip())
|
|
4650
|
+
if 'label' in d:
|
|
4651
|
+
seen.add(d['label'])
|
|
4652
|
+
except FileNotFoundError:
|
|
4653
|
+
pass
|
|
4654
|
+
|
|
4655
|
+
# Append old events, deduped
|
|
4656
|
+
old_file = new_file.replace(new_file.split('-')[-1].split('.')[0], old_slug)
|
|
4657
|
+
with open(tmp_file, 'w') as out:
|
|
4658
|
+
# Copy existing new file
|
|
4659
|
+
try:
|
|
4660
|
+
with open(new_file) as f:
|
|
4661
|
+
out.write(f.read())
|
|
4662
|
+
except FileNotFoundError:
|
|
4663
|
+
pass
|
|
4664
|
+
# Append old events not yet seen
|
|
4665
|
+
old_path = '/'.join(new_file.rsplit('/', 1)[:-1] + [f'events-{old_slug}.ndjson'])
|
|
4666
|
+
try:
|
|
4667
|
+
with open(old_path) as f:
|
|
4668
|
+
for line in f:
|
|
4669
|
+
line = line.strip()
|
|
4670
|
+
if not line:
|
|
4671
|
+
continue
|
|
4672
|
+
d = json.loads(line)
|
|
4673
|
+
lid = d.get('label', '')
|
|
4674
|
+
if lid not in seen:
|
|
4675
|
+
seen.add(lid)
|
|
4676
|
+
out.write(json.dumps(d) + '\n')
|
|
4677
|
+
except FileNotFoundError:
|
|
4678
|
+
pass
|
|
4679
|
+
PYEOF
|
|
4680
|
+
|
|
4681
|
+
# Atomic replace
|
|
4682
|
+
mv "$tmp_events" "$events_file"
|
|
4683
|
+
|
|
4684
|
+
# Keep old events as .bak (do not delete)
|
|
4685
|
+
cp "${loop_dir}/events-${old_slug}.ndjson" "${loop_dir}/events-${old_slug}.ndjson.bak"
|
|
4686
|
+
rm "${loop_dir}/events-${old_slug}.ndjson"
|
|
4687
|
+
|
|
4688
|
+
# Migrate runs.jsonl: rewrite project field + dedup by run_id
|
|
4689
|
+
local runs_file="${loop_dir}/runs.jsonl"
|
|
4690
|
+
if [[ -f "$runs_file" ]]; then
|
|
4691
|
+
local tmp; tmp=$(mktemp)
|
|
4692
|
+
python3 - "$old_slug" "$new_slug" "$runs_file" > "$tmp" << 'PYEOF'
|
|
4693
|
+
import json, sys
|
|
4694
|
+
old, new, path = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
4695
|
+
seen = set()
|
|
4696
|
+
with open(path) as f:
|
|
4697
|
+
for line in f:
|
|
4698
|
+
line = line.rstrip('\n')
|
|
4699
|
+
if not line:
|
|
4700
|
+
continue
|
|
4701
|
+
try:
|
|
4702
|
+
d = json.loads(line)
|
|
4703
|
+
if 'project' in d and old in str(d['project']):
|
|
4704
|
+
d['project'] = str(d['project']).replace(old, new)
|
|
4705
|
+
rid = d.get('run_id', '')
|
|
4706
|
+
if rid in seen:
|
|
4707
|
+
continue
|
|
4708
|
+
seen.add(rid)
|
|
4709
|
+
print(json.dumps(d))
|
|
4710
|
+
except Exception:
|
|
4711
|
+
print(line)
|
|
4712
|
+
PYEOF
|
|
4713
|
+
mv "$tmp" "$runs_file"
|
|
4714
|
+
fi
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4392
4717
|
_LOOP_TAG="# roll-loop"
|
|
4393
4718
|
# FIX-065: when sourced in a test context with no explicit override, route
|
|
4394
4719
|
# shared state into a per-process /tmp path instead of falling back to
|
|
@@ -4502,6 +4827,98 @@ _loop_derive_minute() {
|
|
|
4502
4827
|
echo $(( (hash_dec + offset) % 55 + 1 ))
|
|
4503
4828
|
}
|
|
4504
4829
|
|
|
4830
|
+
# US-LOOP-011: validate a (period, offset) pair against the allowed schedule spec.
|
|
4831
|
+
# Allowed periods are the divisors of 60: 60/30/20/15/12/10/6/5.
|
|
4832
|
+
# Offset must be within [0, period).
|
|
4833
|
+
_loop_schedule_valid() {
|
|
4834
|
+
local period="$1" offset="$2"
|
|
4835
|
+
case "$period" in
|
|
4836
|
+
60|30|20|15|12|10|6|5) ;;
|
|
4837
|
+
*) return 1 ;;
|
|
4838
|
+
esac
|
|
4839
|
+
[[ "$offset" =~ ^[0-9]+$ ]] || return 1
|
|
4840
|
+
if (( offset >= period )); then return 1; fi
|
|
4841
|
+
return 0
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
# US-LOOP-011: compute the loop schedule spec for a project.
|
|
4845
|
+
# Resolution order:
|
|
4846
|
+
# 1. .roll/local.yaml loop_schedule.{period_minutes,offset_minute}
|
|
4847
|
+
# 2. ~/.roll/config.yaml loop_minute → period=60, offset=loop_minute
|
|
4848
|
+
# 3. default period=60, offset=hash(project_path)%60
|
|
4849
|
+
# Output: "<period> <offset>" on stdout. Exit 0 on success.
|
|
4850
|
+
# Invalid project config → fallback to global/default + write ALERT.
|
|
4851
|
+
_loop_schedule_spec() {
|
|
4852
|
+
local project_path="$1"
|
|
4853
|
+
|
|
4854
|
+
# 1. Try project-level .roll/local.yaml
|
|
4855
|
+
local local_file="${project_path}/.roll/local.yaml"
|
|
4856
|
+
if [[ -f "$local_file" ]]; then
|
|
4857
|
+
local local_period local_offset
|
|
4858
|
+
# Extract values from under loop_schedule: key (using awk for reliable block parsing)
|
|
4859
|
+
local_period=$(awk '/^loop_schedule:/{found=1;next} found && /^[[:space:]]+period_minutes:/{print $2; exit}' "$local_file")
|
|
4860
|
+
local_offset=$(awk '/^loop_schedule:/{found=1;next} found && /^[[:space:]]+offset_minute:/{print $2; exit}' "$local_file")
|
|
4861
|
+
if [[ -n "$local_period" && -n "$local_offset" ]]; then
|
|
4862
|
+
if _loop_schedule_valid "$local_period" "$local_offset"; then
|
|
4863
|
+
echo "$local_period $local_offset"
|
|
4864
|
+
return 0
|
|
4865
|
+
fi
|
|
4866
|
+
# Invalid: alert, then fall through to global/default
|
|
4867
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
4868
|
+
local alert_file="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/ALERT-${slug}.md"
|
|
4869
|
+
mkdir -p "$(dirname "$alert_file")" 2>/dev/null || true
|
|
4870
|
+
{
|
|
4871
|
+
printf '## ⚠️ US-LOOP-011: Invalid loop_schedule\n\n'
|
|
4872
|
+
printf '**Time**: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
4873
|
+
printf '**Source**: %s\n\n' "${project_path}/.roll/local.yaml"
|
|
4874
|
+
printf '**Values**: period_minutes=%s, offset_minute=%s\n\n' "$local_period" "$local_offset"
|
|
4875
|
+
printf '**Action**: period must be one of 60/30/20/15/12/10/6/5; offset must be 0–(period-1). Falling back to default (period=60).\n\n'
|
|
4876
|
+
printf '%s\n' '---'
|
|
4877
|
+
} >> "$alert_file"
|
|
4878
|
+
fi
|
|
4879
|
+
fi
|
|
4880
|
+
|
|
4881
|
+
# 2. Try global ~/.roll/config.yaml loop_minute (backward compat)
|
|
4882
|
+
local global_minute
|
|
4883
|
+
global_minute=$(_config_read_int "loop_minute" "")
|
|
4884
|
+
if [[ -n "$global_minute" && "$global_minute" =~ ^[0-9]+$ ]]; then
|
|
4885
|
+
echo "60 $global_minute"
|
|
4886
|
+
return 0
|
|
4887
|
+
fi
|
|
4888
|
+
|
|
4889
|
+
# 3. Default: derive from project path hash (never collides across projects)
|
|
4890
|
+
local offset
|
|
4891
|
+
offset=$(_loop_derive_minute "$project_path" 0)
|
|
4892
|
+
echo "60 $offset"
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
# US-LOOP-013: human-readable schedule description for display.
|
|
4896
|
+
# Args: period offset [lang]
|
|
4897
|
+
# lang: en (default) or zh
|
|
4898
|
+
_loop_schedule_desc() {
|
|
4899
|
+
local period="$1" offset="$2" lang="${3:-en}"
|
|
4900
|
+
if [[ "$period" -eq 60 ]]; then
|
|
4901
|
+
if [[ "$lang" == "zh" ]]; then
|
|
4902
|
+
# msg_lang uses the explicit lang param, not ROLL_LANG env; strips trailing
|
|
4903
|
+
# newline via command substitution so callers get a clean string.
|
|
4904
|
+
printf '%s' "$(msg_lang "$lang" agent.hourly_at_02d "$offset")"
|
|
4905
|
+
else
|
|
4906
|
+
printf "every hour :%02d" "$offset"
|
|
4907
|
+
fi
|
|
4908
|
+
return 0
|
|
4909
|
+
fi
|
|
4910
|
+
local times="" slots=$((60 / period)) i m
|
|
4911
|
+
for i in $(seq 0 $((slots - 1))); do
|
|
4912
|
+
m=$((offset + i * period))
|
|
4913
|
+
times="${times} :$(printf '%02d' "$m")"
|
|
4914
|
+
done
|
|
4915
|
+
if [[ "$lang" == "zh" ]]; then
|
|
4916
|
+
printf '%s' "$(msg_lang "$lang" agent.every_d_min_s "$period" "${times# }")"
|
|
4917
|
+
else
|
|
4918
|
+
printf "every %dmin (%s)" "$period" "${times# }"
|
|
4919
|
+
fi
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4505
4922
|
# US-LOOP-001: structured event emission for cycle observability.
|
|
4506
4923
|
# Writes a tab-separated line to stdout (for tmux/attach display) and appends
|
|
4507
4924
|
# a JSON line to the per-project NDJSON event file under _SHARED_ROOT/loop/.
|
|
@@ -4542,7 +4959,7 @@ _loop_event() {
|
|
|
4542
4959
|
startup) _emoji="🚀" ;;
|
|
4543
4960
|
preflight) _emoji="🔍" ;;
|
|
4544
4961
|
worktree_setup) _emoji="🌳" ;;
|
|
4545
|
-
|
|
4962
|
+
agent_invoke) _emoji="🤖" ;;
|
|
4546
4963
|
publish_push) _emoji="📤" ;;
|
|
4547
4964
|
publish_wait_merge) _emoji="⏳" ;;
|
|
4548
4965
|
cleanup) _emoji="🧹" ;;
|
|
@@ -4649,7 +5066,7 @@ _launchd_plist_path() {
|
|
|
4649
5066
|
|
|
4650
5067
|
_write_launchd_plist() {
|
|
4651
5068
|
local plist_path="$1" label="$2" project_path="$3"
|
|
4652
|
-
local
|
|
5069
|
+
local period="$4" offset="$5" hour="$6" runner_script="$7"
|
|
4653
5070
|
|
|
4654
5071
|
# FIX-087 tripwire: last line of defense if some caller explicitly set
|
|
4655
5072
|
# _LAUNCHD_DIR back to the real path (or built plist_path manually) while
|
|
@@ -4676,26 +5093,37 @@ _write_launchd_plist() {
|
|
|
4676
5093
|
local path_value; path_value=$(_detect_path_prepend)
|
|
4677
5094
|
|
|
4678
5095
|
# FIX-105: macOS 26.4 launchd silently refuses to fire StartCalendarInterval
|
|
4679
|
-
# entries that contain BOTH Hour and Minute keys
|
|
4680
|
-
#
|
|
4681
|
-
#
|
|
4682
|
-
#
|
|
4683
|
-
# schedule), emit StartInterval=86400 (24h period) instead. First fire is
|
|
4684
|
-
# bootstrap+24h rather than the exact requested wall-clock time — acceptable
|
|
4685
|
-
# trade since the alternative was "never fires at all" (dream/brief broken
|
|
4686
|
-
# for 4+ days). The Minute/Hour args are still kept in the function signature
|
|
4687
|
-
# for callers that may want to filter at runtime, but they no longer steer
|
|
4688
|
-
# the plist trigger format for daily schedules.
|
|
5096
|
+
# entries that contain BOTH Hour and Minute keys. Daily services use
|
|
5097
|
+
# StartInterval=86400 instead.
|
|
5098
|
+
# US-LOOP-012: when period < 60 and no hour, generate StartCalendarInterval
|
|
5099
|
+
# <array> with one <dict> per trigger minute.
|
|
4689
5100
|
local schedule_xml
|
|
4690
5101
|
if [[ -n "$hour" ]]; then
|
|
4691
5102
|
schedule_xml=" <key>StartInterval</key>
|
|
4692
5103
|
<integer>86400</integer>"
|
|
4693
|
-
|
|
5104
|
+
elif [[ "$period" == "60" ]]; then
|
|
4694
5105
|
schedule_xml=" <key>StartCalendarInterval</key>
|
|
4695
5106
|
<dict>
|
|
4696
5107
|
<key>Minute</key>
|
|
4697
|
-
<integer>${
|
|
5108
|
+
<integer>${offset}</integer>
|
|
4698
5109
|
</dict>"
|
|
5110
|
+
else
|
|
5111
|
+
# US-LOOP-012: period < 60 → generate array of dicts
|
|
5112
|
+
local entries=$(( 60 / period ))
|
|
5113
|
+
local xml_lines=" <key>StartCalendarInterval</key>
|
|
5114
|
+
<array>"
|
|
5115
|
+
local i m
|
|
5116
|
+
for ((i = 0; i < entries; i++)); do
|
|
5117
|
+
m=$(( offset + i * period ))
|
|
5118
|
+
xml_lines+="
|
|
5119
|
+
<dict>
|
|
5120
|
+
<key>Minute</key>
|
|
5121
|
+
<integer>${m}</integer>
|
|
5122
|
+
</dict>"
|
|
5123
|
+
done
|
|
5124
|
+
xml_lines+="
|
|
5125
|
+
</array>"
|
|
5126
|
+
schedule_xml="$xml_lines"
|
|
4699
5127
|
fi
|
|
4700
5128
|
|
|
4701
5129
|
local content
|
|
@@ -4761,30 +5189,15 @@ _write_loop_runner_script() {
|
|
|
4761
5189
|
# US-AUTO-037: strip leading `cd "<path>" && ` (callers like
|
|
4762
5190
|
# _install_launchd_plists prepend it). The runner now manages cwd itself
|
|
4763
5191
|
# — pointing at the worktree when isolation succeeds, project_path otherwise.
|
|
4764
|
-
local
|
|
5192
|
+
local agent_cmd; agent_cmd="${cmd_verbose#cd \"*\" && }"
|
|
4765
5193
|
# FIX-048: Claude Code resolves project root from the worktree's .git file to
|
|
4766
5194
|
# the main repo, placing worktree absolute paths outside its sandbox. Inject
|
|
4767
5195
|
# --add-dir "$WT" so the worktree directory is explicitly allowed. Only applies
|
|
4768
5196
|
# to claude (the --output-format stream-json flag is exclusive to claude runs).
|
|
4769
|
-
if [[ "$
|
|
4770
|
-
|
|
5197
|
+
if [[ "$agent_cmd" == *"--output-format stream-json"* ]]; then
|
|
5198
|
+
agent_cmd="${agent_cmd/--output-format stream-json/--output-format stream-json --add-dir \"\$WT\"}"
|
|
4771
5199
|
fi
|
|
4772
5200
|
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
|
|
4788
5201
|
cat > "$inner_path" << INNER
|
|
4789
5202
|
#!/bin/bash -l
|
|
4790
5203
|
set -o pipefail
|
|
@@ -4816,7 +5229,7 @@ printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
|
4816
5229
|
# to detect stale execution without relying on PID reuse heuristics.
|
|
4817
5230
|
# US-LOOP-007: heartbeat also emits phase_tick for the current phase so tmux
|
|
4818
5231
|
# readers see "still alive in <phase>" during long-running silences (e.g.
|
|
4819
|
-
#
|
|
5232
|
+
# agent_invoke 5-45 min). CURRENT_PHASE is maintained by _phase_begin/_phase_end.
|
|
4820
5233
|
HEARTBEAT_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
|
|
4821
5234
|
CURRENT_PHASE=""
|
|
4822
5235
|
# bash 3.2 (macOS /bin/bash) lacks associative arrays — use namespaced
|
|
@@ -5056,6 +5469,17 @@ _runs_append "failed" 0 "[]" "\$_phases_t" 2>/dev/null || true
|
|
|
5056
5469
|
_phases_t=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_t" ] && _phases_t='{}'
|
|
5057
5470
|
_runs_append "aborted" 0 "[]" "\$_phases_t" 2>/dev/null || true
|
|
5058
5471
|
fi
|
|
5472
|
+
# US-LOOP-015: process cycle raw log — strip ANSI, remove CR, rotate
|
|
5473
|
+
if [ -n "\${ROLL_CYCLE_LOG_RAW:-}" ] && [ -f "\$ROLL_CYCLE_LOG_RAW" ]; then
|
|
5474
|
+
_log_dir="${project_path}/.roll/cycle-logs"
|
|
5475
|
+
mkdir -p "\$_log_dir"
|
|
5476
|
+
sed -E 's/\x1b\[[0-9;]*[A-Za-z]//g; s/\r$//' "\$ROLL_CYCLE_LOG_RAW" \
|
|
5477
|
+
> "\${_log_dir}/\${CYCLE_ID}.log" 2>/dev/null || true
|
|
5478
|
+
rm -f "\$ROLL_CYCLE_LOG_RAW"
|
|
5479
|
+
# Rotate: keep newest ROLL_CYCLE_LOG_KEEP (default 50) .log files
|
|
5480
|
+
_keep="\${ROLL_CYCLE_LOG_KEEP:-50}"
|
|
5481
|
+
( cd "\$_log_dir" && ls -t *.log 2>/dev/null | tail -n +\$((_keep + 1)) | xargs -r rm -f ) 2>/dev/null || true
|
|
5482
|
+
fi
|
|
5059
5483
|
rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
|
|
5060
5484
|
exit "\$_rc"
|
|
5061
5485
|
}
|
|
@@ -5193,11 +5617,7 @@ export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
|
5193
5617
|
# US-LOOP-010: tell loop-fmt.py which agent is running so it can branch
|
|
5194
5618
|
# rendering: claude → stream-json parser, others → transparent passthrough.
|
|
5195
5619
|
export ROLL_LOOP_AGENT="\$(_project_agent)"
|
|
5196
|
-
_phase_begin
|
|
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
|
|
5620
|
+
_phase_begin agent_invoke
|
|
5201
5621
|
for _attempt in 1 2 3; do
|
|
5202
5622
|
# FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
|
|
5203
5623
|
# the SIGTERM result of the previous attempt and would otherwise force an
|
|
@@ -5220,9 +5640,9 @@ for _attempt in 1 2 3; do
|
|
|
5220
5640
|
} ) &
|
|
5221
5641
|
_WATCHDOG_PID=\$!
|
|
5222
5642
|
if [ -f "\$FMT" ]; then
|
|
5223
|
-
( cd "\$WT" && ${
|
|
5643
|
+
( cd "\$WT" && ${agent_cmd} ) | python3 "\$FMT"
|
|
5224
5644
|
else
|
|
5225
|
-
( cd "\$WT" && ${
|
|
5645
|
+
( cd "\$WT" && ${agent_cmd} )
|
|
5226
5646
|
fi
|
|
5227
5647
|
_exit=\$?
|
|
5228
5648
|
kill "\$_WATCHDOG_PID" 2>/dev/null
|
|
@@ -5235,48 +5655,10 @@ for _attempt in 1 2 3; do
|
|
|
5235
5655
|
fi
|
|
5236
5656
|
done
|
|
5237
5657
|
|
|
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
|
-
|
|
5276
5658
|
if [ "\$_CYCLE_TIMED_OUT" -eq 1 ] || [ "\$_exit" -ne 0 ]; then
|
|
5277
|
-
_phase_end
|
|
5659
|
+
_phase_end agent_invoke fail
|
|
5278
5660
|
else
|
|
5279
|
-
_phase_end
|
|
5661
|
+
_phase_end agent_invoke ok
|
|
5280
5662
|
fi
|
|
5281
5663
|
|
|
5282
5664
|
# FIX-057: timed out — skip publish; EXIT trap writes cycle_end blocked + ALERT.
|
|
@@ -5323,6 +5705,14 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
5323
5705
|
if [ "\$_cycle_commits" -eq 0 ]; then
|
|
5324
5706
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
5325
5707
|
_loop_event idle "\${CYCLE_ID}" "" "" || true
|
|
5708
|
+
# FIX-F (2026-05-25): explicitly write the terminal "idle" cycle_end +
|
|
5709
|
+
# runs row here, otherwise the EXIT trap's catch-all fallback (which
|
|
5710
|
+
# writes "aborted" when _CYCLE_END_WRITTEN is still 0) will reclassify
|
|
5711
|
+
# this successful no-op as a failure on the dashboard.
|
|
5712
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "idle" || true
|
|
5713
|
+
_CYCLE_END_WRITTEN=1
|
|
5714
|
+
_phases_idle=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_idle" ] && _phases_idle='{}'
|
|
5715
|
+
_runs_append "idle" 0 "[]" "\$_phases_idle" 2>/dev/null || true
|
|
5326
5716
|
echo "[loop] cycle \${CYCLE_ID}: idle (no new commits); worktree cleaned"
|
|
5327
5717
|
else
|
|
5328
5718
|
_is_doc_only=0
|
|
@@ -5513,7 +5903,10 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
5513
5903
|
tmux kill-session -t "\$_s" 2>/dev/null || true
|
|
5514
5904
|
done
|
|
5515
5905
|
tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
|
|
5516
|
-
|
|
5906
|
+
CYCLE_LOG_RAW="${project_path}/.roll/cycle-logs/.pipe-\$\$.raw"
|
|
5907
|
+
mkdir -p "${project_path}/.roll/cycle-logs"
|
|
5908
|
+
export ROLL_CYCLE_LOG_RAW="\$CYCLE_LOG_RAW"
|
|
5909
|
+
tmux pipe-pane -t "\$SESSION" "tee -a \"\$LOG\" >> \"\$ROLL_CYCLE_LOG_RAW\""
|
|
5517
5910
|
# Auto-attach popup: when not muted, spawn a Terminal.app window attached
|
|
5518
5911
|
# to the tmux session so the user can watch the loop work in real time.
|
|
5519
5912
|
# FIX-054: terminal selection removed — fixed to macOS Terminal.app for
|
|
@@ -5525,7 +5918,14 @@ if command -v tmux >/dev/null 2>&1; then
|
|
|
5525
5918
|
# Microsoft Teams.app).
|
|
5526
5919
|
if [ -z "\${ROLL_LOOP_NO_POPUP:-}" ] && [ -z "\${BATS_TEST_NUMBER:-}" ] && [ ! -f "\$HOME/.shared/roll/loop/mute-${slug}" ] && [ "\$(uname)" = "Darwin" ]; then
|
|
5527
5920
|
_attach_cmd="\$HOME/.shared/roll/loop/attach-\$SESSION.command"
|
|
5528
|
-
|
|
5921
|
+
# Drop \`exec\` so the wrapping shell survives \`tmux attach\` exiting,
|
|
5922
|
+
# then \`read\` to hold the Terminal open until the user has had a
|
|
5923
|
+
# chance to scroll back through the cycle's output. Without this the
|
|
5924
|
+
# window closes the instant the tmux session ends (cycle_end kills
|
|
5925
|
+
# the session) and the entire scrollback disappears with it; the
|
|
5926
|
+
# cron-<slug>.log file still has the full transcript as a fallback.
|
|
5927
|
+
printf '#!/bin/bash\\ntmux attach -t %s\\necho\\necho "================================================================"\\necho " cycle ended. log: ~/.shared/roll/loop/cron-%s.log"\\necho " press enter to close this window."\\necho "================================================================"\\nread _\\n' \\
|
|
5928
|
+
"\$SESSION" "${slug}" > "\$_attach_cmd" 2>/dev/null || true
|
|
5529
5929
|
chmod +x "\$_attach_cmd" 2>/dev/null || true
|
|
5530
5930
|
open -g -a Terminal "\$_attach_cmd" >/dev/null 2>&1 || true
|
|
5531
5931
|
fi
|
|
@@ -5625,10 +6025,13 @@ _install_launchd_plists() {
|
|
|
5625
6025
|
mkdir -p "$_LAUNCHD_DIR"
|
|
5626
6026
|
mkdir -p "${shared}/loop" "${shared}/dream" "${shared}/brief"
|
|
5627
6027
|
|
|
5628
|
-
local active_start active_end
|
|
6028
|
+
local active_start active_end dream_hour dream_minute brief_hour brief_minute loop_period loop_offset
|
|
5629
6029
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
5630
6030
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
5631
|
-
|
|
6031
|
+
# US-LOOP-012: use _loop_schedule_spec instead of raw loop_minute
|
|
6032
|
+
local loop_spec; loop_spec=$(_loop_schedule_spec "$project_path")
|
|
6033
|
+
loop_period="${loop_spec%% *}"
|
|
6034
|
+
loop_offset="${loop_spec##* }"
|
|
5632
6035
|
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
5633
6036
|
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
5634
6037
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
@@ -5638,7 +6041,8 @@ _install_launchd_plists() {
|
|
|
5638
6041
|
|
|
5639
6042
|
local services=("loop" "dream" "brief")
|
|
5640
6043
|
local skill_names=("roll-loop" "roll-.dream" "roll-brief")
|
|
5641
|
-
local
|
|
6044
|
+
local periods=("$loop_period" "60" "60")
|
|
6045
|
+
local offsets=("$loop_offset" "$dream_minute" "$brief_minute")
|
|
5642
6046
|
local hours=("" "$dream_hour" "$brief_hour")
|
|
5643
6047
|
|
|
5644
6048
|
local updated=0
|
|
@@ -5646,10 +6050,14 @@ _install_launchd_plists() {
|
|
|
5646
6050
|
# FIX-058: after FIX-056 introduced realpath normalization, the slug for an
|
|
5647
6051
|
# existing project may have changed. Migrate state before creating new files.
|
|
5648
6052
|
_slug_migrate_from_legacy "$slug" "${shared}/loop"
|
|
6053
|
+
# US-OBS-010: when slug changed from path-based to remote-based, merge
|
|
6054
|
+
# old records into the new identity (dedup, atomic, .bak backup)
|
|
6055
|
+
_slug_migrate_to_remote "$project_path" "${shared}/loop"
|
|
5649
6056
|
for i in "${!services[@]}"; do
|
|
5650
6057
|
local svc="${services[$i]}"
|
|
5651
6058
|
local skill="${skill_names[$i]}"
|
|
5652
|
-
local
|
|
6059
|
+
local period="${periods[$i]}"
|
|
6060
|
+
local offset="${offsets[$i]}"
|
|
5653
6061
|
local hour="${hours[$i]}"
|
|
5654
6062
|
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
5655
6063
|
local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
|
|
@@ -5666,7 +6074,7 @@ _install_launchd_plists() {
|
|
|
5666
6074
|
|
|
5667
6075
|
local before=""
|
|
5668
6076
|
[[ -f "$plist" ]] && before=$(cat "$plist")
|
|
5669
|
-
_write_launchd_plist "$plist" "$label" "$project_path" "$
|
|
6077
|
+
_write_launchd_plist "$plist" "$label" "$project_path" "$period" "$offset" "$hour" "$runner"
|
|
5670
6078
|
local after; after=$(cat "$plist")
|
|
5671
6079
|
if [[ "$before" != "$after" ]]; then
|
|
5672
6080
|
updated=$((updated + 1))
|
|
@@ -5695,10 +6103,10 @@ _install_launchd_plists() {
|
|
|
5695
6103
|
done
|
|
5696
6104
|
|
|
5697
6105
|
if [[ $updated -gt 0 ]]; then
|
|
5698
|
-
ok "
|
|
5699
|
-
echo "
|
|
6106
|
+
ok "$(msg agent.launchd_plists_installed_updated_launchagents ${updated})"
|
|
6107
|
+
echo "$(msg agent.run_roll_loop_on_to_activate)"
|
|
5700
6108
|
else
|
|
5701
|
-
ok "
|
|
6109
|
+
ok "$(msg agent.launchd_plists_up_to_date_launchagents)"
|
|
5702
6110
|
fi
|
|
5703
6111
|
}
|
|
5704
6112
|
|
|
@@ -5735,6 +6143,8 @@ cmd_loop() {
|
|
|
5735
6143
|
status) _loop_status "$@" ;;
|
|
5736
6144
|
monitor) _loop_monitor "${1:-3}" ;;
|
|
5737
6145
|
runs) _loop_runs "$@" ;;
|
|
6146
|
+
log) _loop_log "$@" ;;
|
|
6147
|
+
story) _loop_story "$@" ;;
|
|
5738
6148
|
events) _loop_event_log "${1:-20}" ;;
|
|
5739
6149
|
attach) _loop_attach ;;
|
|
5740
6150
|
mute) _loop_mute ;;
|
|
@@ -5746,7 +6156,39 @@ cmd_loop() {
|
|
|
5746
6156
|
enforce-tcr) _loop_enforce_tcr "${1:-}" "${2:-}" ;;
|
|
5747
6157
|
precheck-ci) _loop_precheck_ci ;;
|
|
5748
6158
|
branches) _loop_branches "$(pwd -P)" ;;
|
|
5749
|
-
*)
|
|
6159
|
+
*) cat <<'HELP'
|
|
6160
|
+
Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mute|unmute|pause|resume|reset|notify|enforce-tcr|precheck-ci|branches>
|
|
6161
|
+
|
|
6162
|
+
on Install launchd scheduler (loop + dream + brief)
|
|
6163
|
+
off Remove launchd scheduler
|
|
6164
|
+
now Run one cycle immediately
|
|
6165
|
+
test Quick smoke test (tmux/popup/stream chain)
|
|
6166
|
+
status Show scheduler state and current loop state
|
|
6167
|
+
monitor Live dashboard: launchd status, queue, recent runs
|
|
6168
|
+
runs [N] Show last N run summaries (default 10)
|
|
6169
|
+
log [id] Show per-cycle log (default: latest; optional cycle-id or prefix)
|
|
6170
|
+
story <ID> Show per-story rollup (cycles, duration, tokens, cost, PRs)
|
|
6171
|
+
events [N] Show last N cycle events (default 20)
|
|
6172
|
+
attach Attach to running loop tmux session (Ctrl-B D to detach)
|
|
6173
|
+
mute Suppress auto-attach popup
|
|
6174
|
+
unmute Re-enable auto-attach popup
|
|
6175
|
+
pause Pause scheduling (keep plist, skip execution)
|
|
6176
|
+
resume Resume scheduling after pause
|
|
6177
|
+
reset Clear loop state (start fresh on next fire)
|
|
6178
|
+
notify Send macOS notification
|
|
6179
|
+
enforce-tcr Verify TCR commit count for a completed story
|
|
6180
|
+
precheck-ci Check HEAD CI status before scanning BACKLOG
|
|
6181
|
+
branches List loop-related branches
|
|
6182
|
+
|
|
6183
|
+
Schedule is configured per-project in .roll/local.yaml:
|
|
6184
|
+
|
|
6185
|
+
loop_schedule:
|
|
6186
|
+
period_minutes: 30 # 60, 30, 20, 15, 12, 10, 6, or 5
|
|
6187
|
+
offset_minute: 7 # 0 – (period_minutes - 1)
|
|
6188
|
+
|
|
6189
|
+
See guide/en/loop.md for full documentation.
|
|
6190
|
+
HELP
|
|
6191
|
+
exit 1 ;;
|
|
5750
6192
|
esac
|
|
5751
6193
|
}
|
|
5752
6194
|
|
|
@@ -5757,7 +6199,16 @@ _loop_on() {
|
|
|
5757
6199
|
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
5758
6200
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
5759
6201
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
5760
|
-
|
|
6202
|
+
# US-LOOP-011: read schedule spec from project or global config
|
|
6203
|
+
local loop_spec loop_period loop_offset
|
|
6204
|
+
loop_spec=$(_loop_schedule_spec "$project_path")
|
|
6205
|
+
loop_period="${loop_spec%% *}"
|
|
6206
|
+
loop_offset="${loop_spec##* }"
|
|
6207
|
+
# Keep loop_minute for Linux crontab backward compat (only supports hourly)
|
|
6208
|
+
loop_minute="$loop_offset"
|
|
6209
|
+
local loop_sched_en loop_sched_zh
|
|
6210
|
+
loop_sched_en=$(_loop_schedule_desc "$loop_period" "$loop_offset" en)
|
|
6211
|
+
loop_sched_zh=$(_loop_schedule_desc "$loop_period" "$loop_offset" zh)
|
|
5761
6212
|
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
5762
6213
|
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
5763
6214
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
@@ -5789,14 +6240,14 @@ _loop_on() {
|
|
|
5789
6240
|
done
|
|
5790
6241
|
|
|
5791
6242
|
if $all_loaded; then
|
|
5792
|
-
warn "
|
|
6243
|
+
warn "$(msg loop.loop_already_enabled_for_this_project)"; return 0
|
|
5793
6244
|
fi
|
|
5794
6245
|
|
|
5795
|
-
ok "
|
|
5796
|
-
printf "
|
|
5797
|
-
"$
|
|
5798
|
-
printf "
|
|
5799
|
-
printf "
|
|
6246
|
+
ok "$(msg loop.loop_enabled)"
|
|
6247
|
+
printf "$(msg loop.roll_loop_s_active_02d_00)" \
|
|
6248
|
+
"$loop_sched_en" "$active_start" "$active_end" "$loop_sched_zh" "$active_start" "$active_end"
|
|
6249
|
+
printf "$(msg loop.roll_dream_daily_at_02d_02d)" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
|
|
6250
|
+
printf "$(msg loop.roll_brief_daily_at_02d_02d)" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
|
|
5800
6251
|
echo " • Agent: ${agent} (change: roll agent use <name>)"
|
|
5801
6252
|
return 0
|
|
5802
6253
|
fi
|
|
@@ -5804,7 +6255,7 @@ _loop_on() {
|
|
|
5804
6255
|
# Linux: crontab
|
|
5805
6256
|
local sd="${ROLL_HOME}/skills"
|
|
5806
6257
|
if crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}"; then
|
|
5807
|
-
warn "
|
|
6258
|
+
warn "$(msg loop.loop_already_enabled_for_this_project_2)"; return 0
|
|
5808
6259
|
fi
|
|
5809
6260
|
|
|
5810
6261
|
mkdir -p "${_SHARED_ROOT}/loop" "${_SHARED_ROOT}/dream" "${_SHARED_ROOT}/brief"
|
|
@@ -5823,11 +6274,11 @@ _loop_on() {
|
|
|
5823
6274
|
printf "%d %d * * * %s %s:%s\n" "$brief_minute" "$brief_hour" "$brief_cmd" "$_LOOP_TAG" "$project_path"
|
|
5824
6275
|
) | crontab -
|
|
5825
6276
|
|
|
5826
|
-
ok "
|
|
5827
|
-
printf "
|
|
5828
|
-
"$
|
|
5829
|
-
printf "
|
|
5830
|
-
printf "
|
|
6277
|
+
ok "$(msg loop.loop_enabled_2)"
|
|
6278
|
+
printf "$(msg loop.roll_loop_s_active_02d_00_2)" \
|
|
6279
|
+
"$loop_sched_en" "$active_start" "$active_end" "$loop_sched_zh" "$active_start" "$active_end"
|
|
6280
|
+
printf "$(msg loop.roll_dream_daily_at_02d_02d_2)" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
|
|
6281
|
+
printf "$(msg loop.roll_brief_daily_at_02d_02d_2)" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
|
|
5831
6282
|
echo " • Agent: ${agent} (change: roll agent use <name>)"
|
|
5832
6283
|
}
|
|
5833
6284
|
|
|
@@ -5848,7 +6299,7 @@ _loop_off() {
|
|
|
5848
6299
|
fi
|
|
5849
6300
|
done
|
|
5850
6301
|
if ! $any_loaded; then
|
|
5851
|
-
warn "
|
|
6302
|
+
warn "$(msg loop.loop_not_enabled_for_this_project)"; return 0
|
|
5852
6303
|
fi
|
|
5853
6304
|
local slug; slug=$(_project_slug "$project_path")
|
|
5854
6305
|
local uid; uid=$(id -u)
|
|
@@ -5867,16 +6318,16 @@ _loop_off() {
|
|
|
5867
6318
|
[[ "$_skip_off" == "1" ]] && continue
|
|
5868
6319
|
_launchctl_safe enable "gui/${uid}/${label}" 2>/dev/null || true
|
|
5869
6320
|
done
|
|
5870
|
-
ok "
|
|
6321
|
+
ok "$(msg loop.loop_disabled)"
|
|
5871
6322
|
return 0
|
|
5872
6323
|
fi
|
|
5873
6324
|
|
|
5874
6325
|
# Linux: crontab
|
|
5875
6326
|
if ! crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}"; then
|
|
5876
|
-
warn "
|
|
6327
|
+
warn "$(msg loop.loop_not_enabled_for_this_project_2)"; return 0
|
|
5877
6328
|
fi
|
|
5878
6329
|
crontab -l 2>/dev/null | grep -v "${_LOOP_TAG}:${project_path}" | crontab -
|
|
5879
|
-
ok "
|
|
6330
|
+
ok "$(msg loop.loop_disabled_2)"
|
|
5880
6331
|
}
|
|
5881
6332
|
|
|
5882
6333
|
_loop_is_active() {
|
|
@@ -5910,9 +6361,9 @@ _loop_now() {
|
|
|
5910
6361
|
# but no live signal exists, this trigger is the canonical recovery point.
|
|
5911
6362
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
5912
6363
|
if _loop_is_active "$slug"; then
|
|
5913
|
-
warn "
|
|
6364
|
+
warn "$(msg loop.loop_already_running_loop)"; return 0
|
|
5914
6365
|
fi
|
|
5915
|
-
info "
|
|
6366
|
+
info "$(msg loop.stale_running_state_detected_healing_before)"
|
|
5916
6367
|
printf "status: idle\n" > "$_LOOP_STATE"
|
|
5917
6368
|
rm -f "${_SHARED_ROOT}/loop/.LOCK-${slug}" 2>/dev/null || true
|
|
5918
6369
|
fi
|
|
@@ -5925,7 +6376,7 @@ _loop_now() {
|
|
|
5925
6376
|
err "Run 'roll setup' or 'roll loop on' first to generate it."
|
|
5926
6377
|
return 1
|
|
5927
6378
|
fi
|
|
5928
|
-
info "
|
|
6379
|
+
info "$(msg loop.starting_new_loop_cycle)"
|
|
5929
6380
|
ROLL_LOOP_FORCE=1 bash "$runner"
|
|
5930
6381
|
# Reset stale running state if the cycle exited without cleanup (e.g. API error, SIGKILL)
|
|
5931
6382
|
if [[ -f "$_LOOP_STATE" ]] && grep -q "^status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
@@ -5970,14 +6421,14 @@ _loop_test() {
|
|
|
5970
6421
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
5971
6422
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
5972
6423
|
|
|
5973
|
-
info "
|
|
6424
|
+
info "$(msg loop.generating_test_runner_agent ${agent})"
|
|
5974
6425
|
_write_loop_runner_script "$test_runner" "$project_path" \
|
|
5975
6426
|
"${agent_cmd}" \
|
|
5976
6427
|
"$log" "$active_start" "$active_end"
|
|
5977
6428
|
|
|
5978
|
-
info "
|
|
6429
|
+
info "$(msg loop.starting_smoke_test_agent ${agent})"
|
|
5979
6430
|
info "Watch for: tmux session + terminal popup + stream-json events flowing"
|
|
5980
|
-
info "
|
|
6431
|
+
info "$(msg loop.observing_tmux_session_terminal_popup_stream)"
|
|
5981
6432
|
|
|
5982
6433
|
local start_time; start_time=$(date +%s)
|
|
5983
6434
|
ROLL_LOOP_FORCE=1 bash "$test_runner"
|
|
@@ -5985,9 +6436,9 @@ _loop_test() {
|
|
|
5985
6436
|
local elapsed=$(( $(date +%s) - start_time ))
|
|
5986
6437
|
|
|
5987
6438
|
if [[ $exit_code -eq 0 ]]; then
|
|
5988
|
-
ok "
|
|
6439
|
+
ok "$(msg loop.smoke_test_passed_s_agent_smoke ${elapsed} ${agent})"
|
|
5989
6440
|
else
|
|
5990
|
-
err "
|
|
6441
|
+
err "$(msg loop.smoke_test_failed_exit_s_agent ${exit_code} ${elapsed} ${agent})"
|
|
5991
6442
|
return 1
|
|
5992
6443
|
fi
|
|
5993
6444
|
}
|
|
@@ -6005,6 +6456,25 @@ _loop_status() {
|
|
|
6005
6456
|
_legacy_loop_status "$@"
|
|
6006
6457
|
}
|
|
6007
6458
|
|
|
6459
|
+
_loop_story() {
|
|
6460
|
+
if [[ -z "${1:-}" || "$1" == "-h" || "$1" == "--help" ]]; then
|
|
6461
|
+
cat <<'HELP'
|
|
6462
|
+
Usage: roll loop story <STORY-ID> [--days N] [--json]
|
|
6463
|
+
|
|
6464
|
+
Show a per-story rollup across cycles: count, span, duration, tokens,
|
|
6465
|
+
cost, model, PR landings, and the last 3 cycles. Story ID is case-
|
|
6466
|
+
insensitive (us-loop-004 == US-LOOP-004).
|
|
6467
|
+
|
|
6468
|
+
Examples:
|
|
6469
|
+
roll loop story US-LOOP-004
|
|
6470
|
+
roll loop story us-loop-004 --days 90
|
|
6471
|
+
roll loop story US-LOOP-004 --json | jq .cost
|
|
6472
|
+
HELP
|
|
6473
|
+
return 1
|
|
6474
|
+
fi
|
|
6475
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-loop-story.py" "$@"
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6008
6478
|
_legacy_loop_status() {
|
|
6009
6479
|
local project_path; project_path=$(pwd -P)
|
|
6010
6480
|
local agent; agent=$(_project_agent)
|
|
@@ -6062,7 +6532,7 @@ _loop_pause() {
|
|
|
6062
6532
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
6063
6533
|
local label; label=$(_launchd_label "loop" "$project_path")
|
|
6064
6534
|
if ! _launchd_is_loaded "$label"; then
|
|
6065
|
-
warn "
|
|
6535
|
+
warn "$(msg loop.loop_not_enabled_nothing_to_pause)"; return 0
|
|
6066
6536
|
fi
|
|
6067
6537
|
# FIX-097: never touch host launchctl from a sandboxed plist path.
|
|
6068
6538
|
if ! _launchd_should_skip_registry; then
|
|
@@ -6076,7 +6546,7 @@ _loop_pause() {
|
|
|
6076
6546
|
|
|
6077
6547
|
mkdir -p "$(dirname "$_LOOP_STATE")"
|
|
6078
6548
|
printf 'status: paused\npaused_at: "%s"\npaused_reason: manual\n' "$paused_at" > "$_LOOP_STATE"
|
|
6079
|
-
ok "
|
|
6549
|
+
ok "$(msg loop.loop_paused)"
|
|
6080
6550
|
}
|
|
6081
6551
|
|
|
6082
6552
|
_loop_resume() {
|
|
@@ -6096,27 +6566,27 @@ _loop_resume() {
|
|
|
6096
6566
|
rm -f "${_SHARED_ROOT}/loop/PAUSE-${slug}"
|
|
6097
6567
|
fi
|
|
6098
6568
|
printf "status: idle\n" > "$_LOOP_STATE"
|
|
6099
|
-
ok "
|
|
6569
|
+
ok "$(msg loop.loop_resumed)"
|
|
6100
6570
|
return 0
|
|
6101
6571
|
fi
|
|
6102
6572
|
|
|
6103
6573
|
# Interrupt resume: loop was running a Story and crashed
|
|
6104
6574
|
if [[ ! -f "$_LOOP_STATE" ]]; then
|
|
6105
|
-
warn "
|
|
6575
|
+
warn "$(msg loop.no_loop_state_found_nothing_to)"; return 0
|
|
6106
6576
|
fi
|
|
6107
6577
|
if grep -q "status: running" "$_LOOP_STATE" 2>/dev/null; then
|
|
6108
|
-
warn "
|
|
6578
|
+
warn "$(msg loop.loop_already_running_loop_2)"; return 0
|
|
6109
6579
|
fi
|
|
6110
|
-
info "
|
|
6580
|
+
info "$(msg loop.resuming_loop_from_last_state)"
|
|
6111
6581
|
_agent_run_skill "roll-loop"
|
|
6112
6582
|
}
|
|
6113
6583
|
|
|
6114
6584
|
_loop_reset() {
|
|
6115
6585
|
if [[ -f "$_LOOP_STATE" ]]; then
|
|
6116
6586
|
rm -f "$_LOOP_STATE"
|
|
6117
|
-
ok "
|
|
6587
|
+
ok "$(msg loop.loop_state_cleared_will_start_fresh)"
|
|
6118
6588
|
else
|
|
6119
|
-
info "
|
|
6589
|
+
info "$(msg loop.no_loop_state_to_clear)"
|
|
6120
6590
|
fi
|
|
6121
6591
|
rm -rf "$(_loop_heal_dir)"
|
|
6122
6592
|
}
|
|
@@ -6127,13 +6597,13 @@ _loop_reset() {
|
|
|
6127
6597
|
_loop_mute() {
|
|
6128
6598
|
mkdir -p "$(dirname "$_LOOP_MUTE_FILE")"
|
|
6129
6599
|
: > "$_LOOP_MUTE_FILE"
|
|
6130
|
-
ok "
|
|
6600
|
+
ok "$(msg loop.muted_auto_attach_disabled)"
|
|
6131
6601
|
}
|
|
6132
6602
|
|
|
6133
6603
|
# Re-enable the auto-attach popup.
|
|
6134
6604
|
_loop_unmute() {
|
|
6135
6605
|
rm -f "$_LOOP_MUTE_FILE"
|
|
6136
|
-
ok "
|
|
6606
|
+
ok "$(msg loop.unmuted_auto_attach_live)"
|
|
6137
6607
|
}
|
|
6138
6608
|
|
|
6139
6609
|
# Attach to the tmux session a running loop iteration writes to. Returns 1 when
|
|
@@ -6144,12 +6614,12 @@ _loop_attach() {
|
|
|
6144
6614
|
local session="roll-loop-${slug}"
|
|
6145
6615
|
|
|
6146
6616
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
6147
|
-
warn "
|
|
6617
|
+
warn "$(msg loop.tmux_not_installed_install_with_brew)"
|
|
6148
6618
|
return 1
|
|
6149
6619
|
fi
|
|
6150
6620
|
|
|
6151
6621
|
if ! tmux has-session -t "$session" 2>/dev/null; then
|
|
6152
|
-
info "
|
|
6622
|
+
info "$(msg loop.no_running_loop_session_for_this)"
|
|
6153
6623
|
info "Wait for next scheduled fire, or run: roll loop now"
|
|
6154
6624
|
return 1
|
|
6155
6625
|
fi
|
|
@@ -6160,7 +6630,7 @@ _loop_attach() {
|
|
|
6160
6630
|
# Pretty-print a duration in seconds as "Xs" / "Ym" / "Yh Zm".
|
|
6161
6631
|
# US-VIEW-019: compute slowest phase + % from a JSON line's phases object.
|
|
6162
6632
|
# Returns "<abbr> <pct>%" (e.g. "claude 97%") or empty when no phases data.
|
|
6163
|
-
# Abbreviations match the AC:
|
|
6633
|
+
# Abbreviations match the AC: agent_invoke→agent, publish_wait_merge→pr-wait,
|
|
6164
6634
|
# publish_push→publish, worktree_setup→worktree; others unchanged.
|
|
6165
6635
|
_loop_runs_slowest_phase() {
|
|
6166
6636
|
local line="$1"
|
|
@@ -6173,7 +6643,7 @@ _loop_runs_slowest_phase() {
|
|
|
6173
6643
|
[ -z "$max_name" ] && return 0
|
|
6174
6644
|
local abbr="$max_name"
|
|
6175
6645
|
case "$max_name" in
|
|
6176
|
-
|
|
6646
|
+
agent_invoke) abbr="agent" ;;
|
|
6177
6647
|
publish_wait_merge) abbr="pr-wait" ;;
|
|
6178
6648
|
publish_push) abbr="publish" ;;
|
|
6179
6649
|
worktree_setup) abbr="worktree" ;;
|
|
@@ -6189,19 +6659,19 @@ _loop_runs_detail() {
|
|
|
6189
6659
|
local cycle_id="$1"
|
|
6190
6660
|
command -v jq >/dev/null 2>&1 || { err "jq required for --detail"; return 1; }
|
|
6191
6661
|
if [[ ! -f "$_LOOP_RUNS" ]]; then
|
|
6192
|
-
echo "
|
|
6662
|
+
echo "$(msg loop.no_runs_jsonl_yet)"
|
|
6193
6663
|
return 0
|
|
6194
6664
|
fi
|
|
6195
6665
|
local row
|
|
6196
6666
|
row=$(jq -c --arg cid "$cycle_id" 'select(.cycle_id == $cid)' "$_LOOP_RUNS" | head -1)
|
|
6197
6667
|
if [[ -z "$row" ]]; then
|
|
6198
|
-
echo "
|
|
6668
|
+
echo "$(msg loop.cycle_not_found $cycle_id)"
|
|
6199
6669
|
return 1
|
|
6200
6670
|
fi
|
|
6201
6671
|
local has_phases
|
|
6202
6672
|
has_phases=$(jq -r '(.phases // {}) | length' <<<"$row")
|
|
6203
6673
|
if [[ "$has_phases" == "0" ]]; then
|
|
6204
|
-
echo "
|
|
6674
|
+
echo "$(msg loop.cycle_has_no_phases_data_pre $cycle_id)"
|
|
6205
6675
|
return 0
|
|
6206
6676
|
fi
|
|
6207
6677
|
echo ""
|
|
@@ -6330,12 +6800,12 @@ _loop_runs() {
|
|
|
6330
6800
|
_loop_backfill_merged >/dev/null 2>&1 || true
|
|
6331
6801
|
|
|
6332
6802
|
if [[ ! -f "$_LOOP_RUNS" ]] || [[ ! -s "$_LOOP_RUNS" ]]; then
|
|
6333
|
-
echo "
|
|
6803
|
+
echo "$(msg loop.no_loop_runs_yet)"
|
|
6334
6804
|
return 0
|
|
6335
6805
|
fi
|
|
6336
6806
|
|
|
6337
6807
|
if ! command -v jq >/dev/null 2>&1; then
|
|
6338
|
-
err "
|
|
6808
|
+
err "$(msg loop.jq_required_for_roll_loop_runs)"
|
|
6339
6809
|
return 1
|
|
6340
6810
|
fi
|
|
6341
6811
|
|
|
@@ -6349,7 +6819,7 @@ _loop_runs() {
|
|
|
6349
6819
|
fi
|
|
6350
6820
|
|
|
6351
6821
|
if [[ -z "$filtered" ]]; then
|
|
6352
|
-
echo "
|
|
6822
|
+
echo "$(msg loop.no_loop_runs_for_current_project)"
|
|
6353
6823
|
return 0
|
|
6354
6824
|
fi
|
|
6355
6825
|
|
|
@@ -6380,12 +6850,83 @@ _notify() {
|
|
|
6380
6850
|
}
|
|
6381
6851
|
|
|
6382
6852
|
# Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
|
|
6853
|
+
# FIX-D 2026-05-25: use --all so commits made on a cycle worktree branch (not
|
|
6854
|
+
# yet merged into the current HEAD) are still counted. Without --all the
|
|
6855
|
+
# enforce-tcr check inside a cycle-worktree cwd would miss the worktree's own
|
|
6856
|
+
# fresh commits when the runner happens to chdir to the main repo before
|
|
6857
|
+
# calling enforce-tcr.
|
|
6383
6858
|
_loop_tcr_count() {
|
|
6384
6859
|
local started_at="$1"
|
|
6385
|
-
git log --oneline --since="${started_at}" 2>/dev/null \
|
|
6860
|
+
git log --all --oneline --since="${started_at}" 2>/dev/null \
|
|
6386
6861
|
| awk '/^[a-f0-9]+ tcr:/{n++} END{print n+0}'
|
|
6387
6862
|
}
|
|
6388
6863
|
|
|
6864
|
+
# US-LOOP-016: display a single cycle log with header.
|
|
6865
|
+
_loop_log_show() {
|
|
6866
|
+
local file="$1"
|
|
6867
|
+
local id; id=$(basename "$file" .log)
|
|
6868
|
+
local ts; ts=$(echo "$id" | sed 's/^\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)-\([0-9]\{2\}\)\([0-9]\{2\}\).*/\1-\2-\3 \4:\5/')
|
|
6869
|
+
printf '# cycle %s · %s\n' "$id" "$ts"
|
|
6870
|
+
cat "$file"
|
|
6871
|
+
}
|
|
6872
|
+
|
|
6873
|
+
# US-LOOP-016: roll loop log [cycle-id] — view per-cycle logs.
|
|
6874
|
+
_loop_log() {
|
|
6875
|
+
local project_path; project_path=$(pwd -P)
|
|
6876
|
+
local logs_dir="${project_path}/.roll/cycle-logs"
|
|
6877
|
+
local query="${1:-}"
|
|
6878
|
+
|
|
6879
|
+
# Directory missing or empty — friendly message.
|
|
6880
|
+
if [[ ! -d "$logs_dir" ]] || [[ -z "$(ls -A "$logs_dir" 2>/dev/null)" ]]; then
|
|
6881
|
+
echo "$(msg loop.no_cycle_logs_found_run_roll)"
|
|
6882
|
+
return 0
|
|
6883
|
+
fi
|
|
6884
|
+
|
|
6885
|
+
if [[ -z "$query" ]]; then
|
|
6886
|
+
# No argument: find latest .log by cycle-id (filename reverse-sort).
|
|
6887
|
+
# Cycle filenames encode the start timestamp (YYYYMMDD-HHMMSS-PID.log) so
|
|
6888
|
+
# the lexicographically greatest name is always the most recent cycle.
|
|
6889
|
+
# mtime-based sorting (ls -t) is unreliable when files are created in the
|
|
6890
|
+
# same sub-second window (all share the same inode timestamp).
|
|
6891
|
+
local latest; latest=$(ls "$logs_dir"/*.log 2>/dev/null | sort -r | head -1)
|
|
6892
|
+
if [[ -z "$latest" ]]; then
|
|
6893
|
+
echo "$(msg loop.no_cycle_logs_found_run_roll_2)"
|
|
6894
|
+
return 0
|
|
6895
|
+
fi
|
|
6896
|
+
_loop_log_show "$latest"
|
|
6897
|
+
return 0
|
|
6898
|
+
fi
|
|
6899
|
+
|
|
6900
|
+
# Exact match first.
|
|
6901
|
+
local exact="${logs_dir}/${query}.log"
|
|
6902
|
+
if [[ -f "$exact" ]]; then
|
|
6903
|
+
_loop_log_show "$exact"
|
|
6904
|
+
return 0
|
|
6905
|
+
fi
|
|
6906
|
+
|
|
6907
|
+
# Prefix match: glob for query*.log files.
|
|
6908
|
+
local matches; matches=$(ls "$logs_dir/${query}"*.log 2>/dev/null || true)
|
|
6909
|
+
local count=0
|
|
6910
|
+
if [[ -n "$matches" ]]; then
|
|
6911
|
+
count=$(echo "$matches" | wc -l | tr -d ' ')
|
|
6912
|
+
count=$((count))
|
|
6913
|
+
fi
|
|
6914
|
+
|
|
6915
|
+
if [[ "$count" -eq 0 ]]; then
|
|
6916
|
+
echo "$(msg loop.no_cycle_log_matching ${query})"
|
|
6917
|
+
return 1
|
|
6918
|
+
elif [[ "$count" -eq 1 ]]; then
|
|
6919
|
+
_loop_log_show "$matches"
|
|
6920
|
+
return 0
|
|
6921
|
+
else
|
|
6922
|
+
echo "$(msg loop.ambiguous_prefix_matches_logs ${query} ${count})"
|
|
6923
|
+
echo "$matches" | while IFS= read -r f; do
|
|
6924
|
+
echo " $(basename "$f" .log)"
|
|
6925
|
+
done
|
|
6926
|
+
return 1
|
|
6927
|
+
fi
|
|
6928
|
+
}
|
|
6929
|
+
|
|
6389
6930
|
# Parse origin remote URL → "owner/repo" for GitHub repos.
|
|
6390
6931
|
# Returns non-zero if no origin, or origin is not github.com.
|
|
6391
6932
|
# Decoupled from `gh` auto-detection so SSH config host rewrites don't break it.
|
|
@@ -6426,24 +6967,24 @@ _ci_wait() {
|
|
|
6426
6967
|
local interval=15
|
|
6427
6968
|
local elapsed=0
|
|
6428
6969
|
|
|
6429
|
-
_gh_available || { warn "
|
|
6970
|
+
_gh_available || { warn "$(msg loop.gh_not_installed_skipping_ci_gate)"; return 0; }
|
|
6430
6971
|
|
|
6431
|
-
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "
|
|
6972
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "$(msg loop.not_a_git_repo)"; return 1; }
|
|
6432
6973
|
local short; short=$(git rev-parse --short HEAD 2>/dev/null)
|
|
6433
6974
|
|
|
6434
6975
|
# Resolve owner/repo from git remote so we don't depend on gh's auto-detection,
|
|
6435
6976
|
# which breaks when ~/.ssh/config rewrites `Host github.com` → IP address.
|
|
6436
6977
|
local repo_slug; repo_slug=$(_gh_repo_slug) || {
|
|
6437
|
-
err "
|
|
6978
|
+
err "$(msg loop.cannot_determine_github_repo_from_origin)"
|
|
6438
6979
|
return 1
|
|
6439
6980
|
}
|
|
6440
6981
|
|
|
6441
|
-
ok "
|
|
6982
|
+
ok "$(msg loop.waiting_for_ci_on ${short} ${repo_slug})"
|
|
6442
6983
|
|
|
6443
6984
|
while (( elapsed < timeout )); do
|
|
6444
6985
|
local runs
|
|
6445
6986
|
runs=$(gh -R "$repo_slug" run list --commit "$commit" --json status,conclusion 2>&1) || {
|
|
6446
|
-
err "
|
|
6987
|
+
err "$(msg loop.gh_run_list_failed_for_gh ${repo_slug} ${short} ${runs})"
|
|
6447
6988
|
return 1
|
|
6448
6989
|
}
|
|
6449
6990
|
|
|
@@ -6455,11 +6996,11 @@ _ci_wait() {
|
|
|
6455
6996
|
local _pr_json; _pr_json=$(gh -R "$repo_slug" pr list --head "$_branch" --state open --json number 2>/dev/null || echo "1")
|
|
6456
6997
|
local _pr_count; _pr_count=$(echo "$_pr_json" | jq 'length' 2>/dev/null || echo "1")
|
|
6457
6998
|
if [[ "$_pr_count" == "0" ]]; then
|
|
6458
|
-
warn "
|
|
6999
|
+
warn "$(msg loop.no_open_pr_for_ci_not ${_branch})"
|
|
6459
7000
|
return 0
|
|
6460
7001
|
fi
|
|
6461
7002
|
fi
|
|
6462
|
-
(( elapsed == 0 )) && echo "
|
|
7003
|
+
(( elapsed == 0 )) && echo "$(msg loop.no_ci_runs_found_yet_waiting)"
|
|
6463
7004
|
sleep "$interval"
|
|
6464
7005
|
elapsed=$(( elapsed + interval ))
|
|
6465
7006
|
continue
|
|
@@ -6469,7 +7010,7 @@ _ci_wait() {
|
|
|
6469
7010
|
pending=$(echo "$runs" | jq -r '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
|
|
6470
7011
|
|
|
6471
7012
|
if [[ "$pending" -gt 0 ]]; then
|
|
6472
|
-
printf "
|
|
7013
|
+
printf "$(msg loop.ci_running_ds_ci)" "$elapsed" "$elapsed"
|
|
6473
7014
|
sleep "$interval"
|
|
6474
7015
|
elapsed=$(( elapsed + interval ))
|
|
6475
7016
|
continue
|
|
@@ -6479,15 +7020,15 @@ _ci_wait() {
|
|
|
6479
7020
|
failed=$(echo "$runs" | jq -r '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != null)] | length' 2>/dev/null || echo "0")
|
|
6480
7021
|
|
|
6481
7022
|
if [[ "$failed" -gt 0 ]]; then
|
|
6482
|
-
err "
|
|
7023
|
+
err "$(msg loop.ci_failed_for_ci ${short})"
|
|
6483
7024
|
return 1
|
|
6484
7025
|
fi
|
|
6485
7026
|
|
|
6486
|
-
ok "
|
|
7027
|
+
ok "$(msg loop.ci_passed_ci)"
|
|
6487
7028
|
return 0
|
|
6488
7029
|
done
|
|
6489
7030
|
|
|
6490
|
-
warn "
|
|
7031
|
+
warn "$(msg loop.ci_timed_out_after_s_ci ${timeout})"
|
|
6491
7032
|
return 1
|
|
6492
7033
|
}
|
|
6493
7034
|
|
|
@@ -6524,14 +7065,14 @@ _loop_precheck_ci() {
|
|
|
6524
7065
|
run_states=$(echo "$runs" \
|
|
6525
7066
|
| jq -r '[.[] | "\(.status // "?")/\(.conclusion // "null")"] | unique | join(", ")' \
|
|
6526
7067
|
2>/dev/null || echo "?")
|
|
6527
|
-
err "
|
|
7068
|
+
err "$(msg loop.pre_run_ci_check_head_ci ${short})"
|
|
6528
7069
|
mkdir -p "$(dirname "$_LOOP_ALERT")"
|
|
6529
7070
|
cat > "$_LOOP_ALERT" << EOF
|
|
6530
7071
|
# ALERT — Pre-run CI check failed (red base)
|
|
6531
7072
|
|
|
6532
7073
|
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
6533
7074
|
**Commit**: ${short}
|
|
6534
|
-
**Reason**:
|
|
7075
|
+
**Reason**: $(msg loop.pre_run_ci_red_base)
|
|
6535
7076
|
**Failing conclusions**: ${failed_conclusions}
|
|
6536
7077
|
**Run states**: ${run_states}
|
|
6537
7078
|
|
|
@@ -6622,7 +7163,7 @@ _loop_enforce_ci() {
|
|
|
6622
7163
|
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
6623
7164
|
**Story**: ${story_id}
|
|
6624
7165
|
**Commit**: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
|
6625
|
-
**Reason**:
|
|
7166
|
+
**Reason**: $(msg loop.ci_did_not_pass_story_kept_in_progress)
|
|
6626
7167
|
|
|
6627
7168
|
**Action required** (choose one):
|
|
6628
7169
|
- Fix CI and re-run: \`roll loop now\`
|
|
@@ -6845,7 +7386,7 @@ _loop_pr_rebase_circuit() {
|
|
|
6845
7386
|
|
|
6846
7387
|
**Time**: $(date '+%Y-%m-%d %H:%M')
|
|
6847
7388
|
**PR**: #${pr}
|
|
6848
|
-
**Reason**:
|
|
7389
|
+
**Reason**: $(msg loop.pr_rebased_within_24h_no_ci "$pr" "$count")
|
|
6849
7390
|
|
|
6850
7391
|
**Action required**:
|
|
6851
7392
|
- Check PR CI logs and workflow files for breakage
|
|
@@ -7386,6 +7927,7 @@ _worktree_sync_meta() {
|
|
|
7386
7927
|
local path="$1"
|
|
7387
7928
|
[ -d ".roll" ] || return 0
|
|
7388
7929
|
rsync -a \
|
|
7930
|
+
--exclude='.git/' \
|
|
7389
7931
|
--exclude='state/' \
|
|
7390
7932
|
--exclude='scratch/' \
|
|
7391
7933
|
--exclude='*.lock' \
|
|
@@ -7416,6 +7958,33 @@ _worktree_merge_back() {
|
|
|
7416
7958
|
_worktree_alert "pull --ff-only origin main failed (remote diverged?)"
|
|
7417
7959
|
return 1
|
|
7418
7960
|
fi
|
|
7961
|
+
# FIX-E (2026-05-25): only doc-only branches may take the ff-merge fast path.
|
|
7962
|
+
# Code changes must go through PR + CI; bypassing that gate has caused
|
|
7963
|
+
# cycle commits to land on main with red CI, undetected. The check is the
|
|
7964
|
+
# same one publish_push uses for the doc-vs-code split; here we apply it
|
|
7965
|
+
# to the loop branch's diff against the (already-pulled) main HEAD so the
|
|
7966
|
+
# bypass is also closed in the gh-unavailable fallback path. The ALERT
|
|
7967
|
+
# body is targeted at the next cycle's agent (human-on-the-loop), telling
|
|
7968
|
+
# it how to retry through the normal flow rather than waiting for a human.
|
|
7969
|
+
local _changed
|
|
7970
|
+
_changed=$(git diff --name-only HEAD.."$branch" 2>/dev/null)
|
|
7971
|
+
if [ -n "$_changed" ] \
|
|
7972
|
+
&& echo "$_changed" | grep -qvE '^(\.roll/|CHANGELOG\.md|guide/|site/|\.claude/|BACKLOG\.md|PROPOSALS\.md|docs/)'; then
|
|
7973
|
+
_worktree_alert "$(printf '%s\n' \
|
|
7974
|
+
"## PRIOR CYCLE FAILED PUBLISH — REQUIRES NORMAL PR FLOW (FIX-E)" \
|
|
7975
|
+
"branch: ${branch}" \
|
|
7976
|
+
"reason: _loop_publish_pr failed (gh unavailable / API error); ff-merge" \
|
|
7977
|
+
" fallback was refused because the branch contains code changes." \
|
|
7978
|
+
" Code must not bypass PR+CI." \
|
|
7979
|
+
"" \
|
|
7980
|
+
"next-cycle action (agent reads this and retries automatically):" \
|
|
7981
|
+
" 1. SKIP normal Todo scan this cycle." \
|
|
7982
|
+
" 2. git push origin ${branch}" \
|
|
7983
|
+
" 3. gh pr create --base main --head ${branch}" \
|
|
7984
|
+
" 4. gh pr merge ${branch} --auto --squash --delete-branch" \
|
|
7985
|
+
" 5. exit cleanly so CI runs and auto-merge takes over.")"
|
|
7986
|
+
return 1
|
|
7987
|
+
fi
|
|
7419
7988
|
if ! git merge --ff-only "$branch" --quiet 2>/dev/null; then
|
|
7420
7989
|
_worktree_alert "merge --ff-only ${branch} failed (not fast-forwardable from main)"
|
|
7421
7990
|
return 1
|
|
@@ -7793,19 +8362,24 @@ _loop_monitor() {
|
|
|
7793
8362
|
echo -e "\n ${BOLD}${CYAN}roll loop monitor${NC} ${YELLOW}${project_name}${NC} ${now} (Ctrl-C to exit)\n"
|
|
7794
8363
|
|
|
7795
8364
|
# Services status (three services on macOS, single on Linux)
|
|
7796
|
-
echo -e "
|
|
8365
|
+
echo -e "$(msg loop.services ${BOLD} ${NC} ${CYAN} ${agent})"
|
|
7797
8366
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
7798
|
-
local active_start active_end
|
|
8367
|
+
local active_start active_end dream_hour dream_minute brief_hour brief_minute
|
|
7799
8368
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
7800
8369
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
7801
|
-
|
|
8370
|
+
# US-LOOP-013: use schedule spec for display
|
|
8371
|
+
local loop_spec loop_period loop_offset
|
|
8372
|
+
loop_spec=$(_loop_schedule_spec "$project_path")
|
|
8373
|
+
loop_period="${loop_spec%% *}"
|
|
8374
|
+
loop_offset="${loop_spec##* }"
|
|
7802
8375
|
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
7803
8376
|
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
7804
8377
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
7805
8378
|
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
7806
8379
|
|
|
7807
8380
|
local loop_sched dream_sched brief_sched
|
|
7808
|
-
loop_sched=$(
|
|
8381
|
+
loop_sched=$(_loop_schedule_desc "$loop_period" "$loop_offset" en)
|
|
8382
|
+
loop_sched="${loop_sched} active ${active_start}:00–${active_end}:00"
|
|
7809
8383
|
dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
|
|
7810
8384
|
brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
|
|
7811
8385
|
|
|
@@ -7815,9 +8389,9 @@ _loop_monitor() {
|
|
|
7815
8389
|
local svc="${svcs[$i]}" schedule="${scheds[$i]}"
|
|
7816
8390
|
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
7817
8391
|
case "$state" in
|
|
7818
|
-
enabled) printf " ${GREEN}%-8s
|
|
7819
|
-
installed-off) printf " ${YELLOW}%-8s
|
|
7820
|
-
not-installed) printf " ${RED}%-8s
|
|
8392
|
+
enabled) printf " ${GREEN}%-8s %s${NC} (%s)\n" "$svc" "$(msg loop.svc_enabled)" "$schedule" ;;
|
|
8393
|
+
installed-off) printf " ${YELLOW}%-8s %s${NC} (%s) %s\n" "$svc" "$(msg loop.svc_installed_off)" "$schedule" "$(msg loop.svc_enabled_run)" ;;
|
|
8394
|
+
not-installed) printf " ${RED}%-8s %s${NC} (%s) %s\n" "$svc" "$(msg loop.svc_not_installed)" "$schedule" "$(msg loop.svc_not_installed_run)" ;;
|
|
7821
8395
|
esac
|
|
7822
8396
|
done
|
|
7823
8397
|
else
|
|
@@ -7856,7 +8430,7 @@ _loop_monitor() {
|
|
|
7856
8430
|
|
|
7857
8431
|
# Queue: pending items
|
|
7858
8432
|
echo ""
|
|
7859
|
-
echo -e "
|
|
8433
|
+
echo -e "$(msg loop.queue ${BOLD} ${NC})"
|
|
7860
8434
|
local backlog=".roll/backlog.md"
|
|
7861
8435
|
if [[ -f "$backlog" ]]; then
|
|
7862
8436
|
local queue_count=0
|
|
@@ -7904,7 +8478,7 @@ _loop_monitor() {
|
|
|
7904
8478
|
local log_file="${_SHARED_ROOT}/loop/launchd.log"
|
|
7905
8479
|
echo ""
|
|
7906
8480
|
echo -e " ─────────────────────────────────────────────────────"
|
|
7907
|
-
echo -e "
|
|
8481
|
+
echo -e "$(msg loop.log_tail ${BOLD} ${NC})"
|
|
7908
8482
|
if [[ -f "$log_file" && -s "$log_file" ]]; then
|
|
7909
8483
|
tail -10 "$log_file" | sed 's/^/ /'
|
|
7910
8484
|
else
|
|
@@ -7916,7 +8490,7 @@ _loop_monitor() {
|
|
|
7916
8490
|
local evfile="${_SHARED_ROOT}/loop/events-${slug}.ndjson"
|
|
7917
8491
|
echo ""
|
|
7918
8492
|
echo -e " ─────────────────────────────────────────────────────"
|
|
7919
|
-
echo -e "
|
|
8493
|
+
echo -e "$(msg loop.cycle_events ${BOLD} ${NC})"
|
|
7920
8494
|
if [[ -f "$evfile" && -s "$evfile" ]]; then
|
|
7921
8495
|
tail -n 10 "$evfile" | python3 -c "
|
|
7922
8496
|
import sys, json
|
|
@@ -7971,7 +8545,7 @@ cmd_brief() {
|
|
|
7971
8545
|
local latest; latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
7972
8546
|
|
|
7973
8547
|
if [[ -z "$latest" ]]; then
|
|
7974
|
-
info "
|
|
8548
|
+
info "$(msg brief.no_brief_yet_generating)"
|
|
7975
8549
|
_agent_run_skill "roll-brief"
|
|
7976
8550
|
latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
7977
8551
|
else
|
|
@@ -7979,7 +8553,7 @@ cmd_brief() {
|
|
|
7979
8553
|
mod_time=$(_file_mtime "$latest")
|
|
7980
8554
|
now=$(date +%s); age=$(( now - mod_time ))
|
|
7981
8555
|
if (( age > 86400 )); then
|
|
7982
|
-
info "
|
|
8556
|
+
info "$(msg brief.brief_is_age_3600_h_old "$(( age / 3600 ))")"
|
|
7983
8557
|
_agent_run_skill "roll-brief"
|
|
7984
8558
|
latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
7985
8559
|
fi
|
|
@@ -8074,19 +8648,19 @@ cmd_alert() {
|
|
|
8074
8648
|
case "$subcmd" in
|
|
8075
8649
|
list|"")
|
|
8076
8650
|
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
8077
|
-
ok "
|
|
8651
|
+
ok "$(msg alert.no_active_alerts)"
|
|
8078
8652
|
return 0
|
|
8079
8653
|
fi
|
|
8080
|
-
echo -e "${BOLD}
|
|
8654
|
+
echo -e "$(msg alert.active_alert ${BOLD} ${NC})"
|
|
8081
8655
|
echo ""
|
|
8082
8656
|
cat "$_LOOP_ALERT"
|
|
8083
8657
|
echo ""
|
|
8084
8658
|
echo -e " Run '${CYAN}roll alert ack${NC}' to acknowledge, '${CYAN}roll alert resolve${NC}' to clear."
|
|
8085
|
-
echo -e "
|
|
8659
|
+
echo -e "$(msg alert.run_roll_alert_ack_to_acknowledge)"
|
|
8086
8660
|
;;
|
|
8087
8661
|
ack)
|
|
8088
8662
|
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
8089
|
-
warn "
|
|
8663
|
+
warn "$(msg alert.no_active_alerts_to_acknowledge)"
|
|
8090
8664
|
return 0
|
|
8091
8665
|
fi
|
|
8092
8666
|
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
|
|
@@ -8094,20 +8668,20 @@ cmd_alert() {
|
|
|
8094
8668
|
echo ""
|
|
8095
8669
|
echo "**Acknowledged**: ${ts}"
|
|
8096
8670
|
} >> "$_LOOP_ALERT"
|
|
8097
|
-
ok "
|
|
8671
|
+
ok "$(msg alert.alert_acknowledged_at ${ts})"
|
|
8098
8672
|
;;
|
|
8099
8673
|
resolve|clear)
|
|
8100
8674
|
if [[ ! -f "$_LOOP_ALERT" ]]; then
|
|
8101
|
-
ok "
|
|
8675
|
+
ok "$(msg alert.no_active_alerts_2)"
|
|
8102
8676
|
return 0
|
|
8103
8677
|
fi
|
|
8104
8678
|
rm -f "$_LOOP_ALERT"
|
|
8105
|
-
ok "
|
|
8679
|
+
ok "$(msg alert.alert_resolved_and_cleared)"
|
|
8106
8680
|
;;
|
|
8107
8681
|
*)
|
|
8108
|
-
err "
|
|
8682
|
+
err "$(msg alert.unknown_subcommand $subcmd)"
|
|
8109
8683
|
echo " Usage: roll alert [list|ack|resolve]"
|
|
8110
|
-
echo "
|
|
8684
|
+
echo "$(msg alert.usage_roll_alert_list_ack_resolve)"
|
|
8111
8685
|
return 1
|
|
8112
8686
|
;;
|
|
8113
8687
|
esac
|
|
@@ -8144,7 +8718,7 @@ cmd_lang() {
|
|
|
8144
8718
|
printf 'lang: %s\n' "$arg" >> "$tmp"
|
|
8145
8719
|
mv "$tmp" "$ROLL_CONFIG"
|
|
8146
8720
|
unset ROLL_LANG_RESOLVED
|
|
8147
|
-
ok "
|
|
8721
|
+
ok "$(msg lang.language_set_to ${arg})"
|
|
8148
8722
|
;;
|
|
8149
8723
|
--reset)
|
|
8150
8724
|
if [[ -f "$ROLL_CONFIG" ]]; then
|
|
@@ -8153,12 +8727,12 @@ cmd_lang() {
|
|
|
8153
8727
|
mv "$tmp" "$ROLL_CONFIG"
|
|
8154
8728
|
fi
|
|
8155
8729
|
unset ROLL_LANG_RESOLVED
|
|
8156
|
-
ok "
|
|
8730
|
+
ok "$(msg lang.language_preference_cleared_will_follow_locale)"
|
|
8157
8731
|
;;
|
|
8158
8732
|
*)
|
|
8159
|
-
err "
|
|
8733
|
+
err "$(msg lang.unknown_language ${arg})"
|
|
8160
8734
|
echo " Valid values: zh, en, --reset"
|
|
8161
|
-
echo "
|
|
8735
|
+
echo "$(msg lang.options_zh_en_reset)"
|
|
8162
8736
|
return 1
|
|
8163
8737
|
;;
|
|
8164
8738
|
esac
|
|
@@ -8174,7 +8748,7 @@ cmd_ci() {
|
|
|
8174
8748
|
case "$1" in
|
|
8175
8749
|
--wait) wait_mode=true; shift ;;
|
|
8176
8750
|
--timeout=*) timeout="${1#*=}"; shift ;;
|
|
8177
|
-
*) err "
|
|
8751
|
+
*) err "$(msg ci.usage_roll_ci_wait_timeout_n)"; exit 1 ;;
|
|
8178
8752
|
esac
|
|
8179
8753
|
done
|
|
8180
8754
|
|
|
@@ -8183,12 +8757,12 @@ cmd_ci() {
|
|
|
8183
8757
|
return
|
|
8184
8758
|
fi
|
|
8185
8759
|
|
|
8186
|
-
_gh_available || { warn "
|
|
8187
|
-
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "
|
|
8760
|
+
_gh_available || { warn "$(msg ci.gh_not_installed_gh)"; return 0; }
|
|
8761
|
+
local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "$(msg ci.not_a_git_repo)"; return 1; }
|
|
8188
8762
|
local runs
|
|
8189
8763
|
runs=$(gh run list --commit "$commit" --json status,conclusion,name 2>/dev/null) || { warn "gh run list failed"; return 0; }
|
|
8190
8764
|
if [[ -z "$runs" || "$runs" == "[]" ]]; then
|
|
8191
|
-
echo "
|
|
8765
|
+
echo "$(msg ci.no_ci_runs_for_git_rev "${commit:0:7}")"
|
|
8192
8766
|
return 0
|
|
8193
8767
|
fi
|
|
8194
8768
|
echo "$runs" | jq -r '.[] | "\(.name): \(.status)/\(.conclusion)"'
|
|
@@ -8278,12 +8852,12 @@ _backlog_lint() {
|
|
|
8278
8852
|
if [ "$violations" -gt 0 ]; then
|
|
8279
8853
|
echo " ${violations} violation(s) — see conventions/global/AGENTS.md §4"
|
|
8280
8854
|
if [ "$gate" = 1 ]; then
|
|
8281
|
-
echo "
|
|
8855
|
+
echo "$(msg ci.gate_enabled_exiting_1 ${violations})"
|
|
8282
8856
|
return 1
|
|
8283
8857
|
fi
|
|
8284
|
-
echo "
|
|
8858
|
+
echo "$(msg ci.phase_1_warn_only_not_blocking ${violations})"
|
|
8285
8859
|
else
|
|
8286
|
-
echo "
|
|
8860
|
+
echo "$(msg ci.no_violations)"
|
|
8287
8861
|
fi
|
|
8288
8862
|
return 0
|
|
8289
8863
|
}
|
|
@@ -8291,7 +8865,7 @@ _backlog_lint() {
|
|
|
8291
8865
|
cmd_backlog() {
|
|
8292
8866
|
local backlog=".roll/backlog.md"
|
|
8293
8867
|
if [[ ! -f "$backlog" ]]; then
|
|
8294
|
-
err "
|
|
8868
|
+
err "$(msg backlog.roll_backlog_md_not_found_run)"
|
|
8295
8869
|
return 1
|
|
8296
8870
|
fi
|
|
8297
8871
|
|
|
@@ -8316,7 +8890,7 @@ cmd_backlog() {
|
|
|
8316
8890
|
local pattern="${2:-}"
|
|
8317
8891
|
local reason="${3:-}"
|
|
8318
8892
|
if [[ -z "$pattern" ]]; then
|
|
8319
|
-
err "
|
|
8893
|
+
err "$(msg backlog.usage_roll_backlog_pattern_reason $subcmd)"
|
|
8320
8894
|
return 1
|
|
8321
8895
|
fi
|
|
8322
8896
|
local new_status
|
|
@@ -8328,9 +8902,9 @@ cmd_backlog() {
|
|
|
8328
8902
|
local count
|
|
8329
8903
|
count=$(_backlog_set_status "$pattern" "$new_status")
|
|
8330
8904
|
if [[ "$count" -eq 0 ]]; then
|
|
8331
|
-
echo "
|
|
8905
|
+
echo "$(msg backlog.no_items_matched $pattern)"
|
|
8332
8906
|
else
|
|
8333
|
-
echo "
|
|
8907
|
+
echo "$(msg backlog.updated_item_s ${count} ${new_status})"
|
|
8334
8908
|
fi
|
|
8335
8909
|
return
|
|
8336
8910
|
;;
|
|
@@ -8375,22 +8949,22 @@ cmd_backlog() {
|
|
|
8375
8949
|
[[ -z "$unknown_items" ]] && unknown_count=0
|
|
8376
8950
|
|
|
8377
8951
|
echo ""
|
|
8378
|
-
echo -e "
|
|
8952
|
+
echo -e "$(msg backlog.pending_backlog ${BOLD} ${NC} ${total})"
|
|
8379
8953
|
echo ""
|
|
8380
8954
|
|
|
8381
|
-
[[ $fix_count -gt 0 ]] && _backlog_render_group "
|
|
8382
|
-
[[ $us_count -gt 0 ]] && _backlog_render_group "
|
|
8383
|
-
[[ $refactor_count -gt 0 ]] && _backlog_render_group "
|
|
8384
|
-
[[ $idea_count -gt 0 ]] && _backlog_render_group "
|
|
8955
|
+
[[ $fix_count -gt 0 ]] && _backlog_render_group "$(msg backlog.bug_fixes)" "$RED" "$fix_count" 12 "$fix_items"
|
|
8956
|
+
[[ $us_count -gt 0 ]] && _backlog_render_group "$(msg backlog.user_stories)" "$CYAN" "$us_count" 14 "$us_items"
|
|
8957
|
+
[[ $refactor_count -gt 0 ]] && _backlog_render_group "$(msg backlog.refactors)" "$YELLOW" "$refactor_count" 16 "$refactor_items"
|
|
8958
|
+
[[ $idea_count -gt 0 ]] && _backlog_render_group "$(msg backlog.ideas)" "$NC" "$idea_count" 14 "$idea_items"
|
|
8385
8959
|
|
|
8386
8960
|
if [[ $total -eq 0 ]]; then
|
|
8387
|
-
echo -e "
|
|
8961
|
+
echo -e "$(msg backlog.nothing_pending_backlog_is_clear ${GREEN} ${NC})"
|
|
8388
8962
|
echo ""
|
|
8389
8963
|
fi
|
|
8390
8964
|
|
|
8391
8965
|
# ── Blocked ───────────────────────────────────────────────────────────────
|
|
8392
8966
|
if [[ $blocked_count -gt 0 ]]; then
|
|
8393
|
-
echo -e "
|
|
8967
|
+
echo -e "$(msg backlog.blocked ${DIM} ${blocked_count} ${NC})"
|
|
8394
8968
|
while IFS= read -r line; do
|
|
8395
8969
|
[[ -z "$line" ]] && continue
|
|
8396
8970
|
local id desc reason
|
|
@@ -8406,7 +8980,7 @@ cmd_backlog() {
|
|
|
8406
8980
|
|
|
8407
8981
|
# ── Deferred ──────────────────────────────────────────────────────────────
|
|
8408
8982
|
if [[ $deferred_count -gt 0 ]]; then
|
|
8409
|
-
echo -e "
|
|
8983
|
+
echo -e "$(msg backlog.deferred ${DIM} ${deferred_count} ${NC})"
|
|
8410
8984
|
while IFS= read -r line; do
|
|
8411
8985
|
[[ -z "$line" ]] && continue
|
|
8412
8986
|
local id desc reason
|
|
@@ -8422,8 +8996,8 @@ cmd_backlog() {
|
|
|
8422
8996
|
|
|
8423
8997
|
# ── Unknown status (show for human/AI triage) ─────────────────────────────
|
|
8424
8998
|
if [[ $unknown_count -gt 0 ]]; then
|
|
8425
|
-
echo -e "
|
|
8426
|
-
echo -e "
|
|
8999
|
+
echo -e "$(msg backlog.unknown_status ${YELLOW} ${unknown_count} ${NC})"
|
|
9000
|
+
echo -e "$(msg backlog.fix_roll_backlog_block_defer_unblock ${YELLOW} ${NC})"
|
|
8427
9001
|
while IFS= read -r line; do
|
|
8428
9002
|
[[ -z "$line" ]] && continue
|
|
8429
9003
|
local id desc status_raw
|
|
@@ -8650,7 +9224,7 @@ _legacy_home() {
|
|
|
8650
9224
|
echo ""
|
|
8651
9225
|
|
|
8652
9226
|
# ── ② AI 自治 — 三层 × 四道防线 (主视觉) ────────────────────────────────
|
|
8653
|
-
echo -e "
|
|
9227
|
+
echo -e "$(msg backlog.ai ${BOLD} ${NC})"
|
|
8654
9228
|
|
|
8655
9229
|
# Loop layer
|
|
8656
9230
|
local loop_state="not-installed"
|
|
@@ -8661,17 +9235,22 @@ _legacy_home() {
|
|
|
8661
9235
|
else
|
|
8662
9236
|
crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_state="enabled"
|
|
8663
9237
|
fi
|
|
8664
|
-
local active_start active_end
|
|
9238
|
+
local active_start active_end dream_hour dream_minute brief_hour brief_minute
|
|
8665
9239
|
active_start=$(_config_read_int "loop_active_start" "10")
|
|
8666
9240
|
active_end=$(_config_read_int "loop_active_end" "18")
|
|
8667
|
-
|
|
9241
|
+
# US-LOOP-013: use schedule spec for display
|
|
9242
|
+
local loop_spec loop_period loop_offset
|
|
9243
|
+
loop_spec=$(_loop_schedule_spec "$project_path")
|
|
9244
|
+
loop_period="${loop_spec%% *}"
|
|
9245
|
+
loop_offset="${loop_spec##* }"
|
|
8668
9246
|
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
8669
9247
|
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
8670
9248
|
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
8671
9249
|
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
8672
9250
|
|
|
8673
9251
|
local loop_badge loop_sched
|
|
8674
|
-
loop_sched=$(
|
|
9252
|
+
loop_sched=$(_loop_schedule_desc "$loop_period" "$loop_offset" en)
|
|
9253
|
+
loop_sched="${loop_sched} active ${active_start}:00–${active_end}:00"
|
|
8675
9254
|
case "$loop_state" in
|
|
8676
9255
|
enabled) loop_badge="${GREEN}● enabled${NC}" ;;
|
|
8677
9256
|
installed-off) loop_badge="${YELLOW}⚠ off${NC}" ;;
|
|
@@ -8733,7 +9312,7 @@ _legacy_home() {
|
|
|
8733
9312
|
fi
|
|
8734
9313
|
|
|
8735
9314
|
# 四道防线
|
|
8736
|
-
echo -e "
|
|
9315
|
+
echo -e "$(msg backlog. ${BOLD} ${NC})"
|
|
8737
9316
|
local def_tcr="${RED}○${NC}" def_review="${GREEN}●${NC}" def_spar="${YELLOW}○${NC}" def_sentinel="${YELLOW}○ off${NC}"
|
|
8738
9317
|
if [[ -n "$last_tcr_min" ]]; then
|
|
8739
9318
|
def_tcr="${GREEN}● ${last_tcr_min}min${NC}"
|
|
@@ -8775,7 +9354,7 @@ _legacy_home() {
|
|
|
8775
9354
|
printf " ${BOLD}📊 Current Focus · DoD${NC}\n"
|
|
8776
9355
|
printf " 🔨 ${BOLD}%s${NC} %s\n" "$p_id" "$p_desc"
|
|
8777
9356
|
printf " [%b] [%b]\n" "$ac_badge" "$ci_badge"
|
|
8778
|
-
printf "
|
|
9357
|
+
printf "$(msg backlog.4_dod_see_us_auto_030 ${YELLOW} ${NC})"
|
|
8779
9358
|
echo ""
|
|
8780
9359
|
fi
|
|
8781
9360
|
|
|
@@ -8784,9 +9363,9 @@ _legacy_home() {
|
|
|
8784
9363
|
alerts=$(_dash_alert_count); alerts=${alerts//[^0-9]/}; alerts=${alerts:-0}
|
|
8785
9364
|
proposals=$(_dash_proposal_count); proposals=${proposals//[^0-9]/}; proposals=${proposals:-0}
|
|
8786
9365
|
release_ready=false; _dash_release_ready && release_ready=true
|
|
8787
|
-
printf "
|
|
9366
|
+
printf "$(msg backlog.n ${BOLD} ${NC})"
|
|
8788
9367
|
if (( alerts == 0 )) && (( proposals == 0 )) && ! $release_ready; then
|
|
8789
|
-
printf "
|
|
9368
|
+
printf "$(msg backlog.ai_2 ${GREEN} ${NC})"
|
|
8790
9369
|
else
|
|
8791
9370
|
(( alerts > 0 )) && printf " ${RED}⚠ %s ALERT${NC} run: roll alert\n" "$alerts"
|
|
8792
9371
|
(( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} see: .roll/proposals.md\n" "$proposals"
|
|
@@ -8796,8 +9375,9 @@ _legacy_home() {
|
|
|
8796
9375
|
|
|
8797
9376
|
# ── ⑥ Schedules & Last Brief ──────────────────────────────────────────────
|
|
8798
9377
|
printf " ${BOLD}⏰ Schedules & Last Brief${NC}\n"
|
|
8799
|
-
|
|
8800
|
-
|
|
9378
|
+
local loop_sched_short; loop_sched_short=$(_loop_schedule_desc "$loop_period" "$loop_offset" en)
|
|
9379
|
+
printf " %s · dream %02d:%02d · brief %02d:%02d\n" \
|
|
9380
|
+
"$loop_sched_short" "$dream_hour" "$dream_minute" "$brief_hour" "$brief_minute"
|
|
8801
9381
|
local latest_brief; latest_brief=$(ls .roll/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
8802
9382
|
if [[ -n "$latest_brief" ]]; then
|
|
8803
9383
|
local mod_time now age summary
|
|
@@ -8833,37 +9413,37 @@ _legacy_help() {
|
|
|
8833
9413
|
echo -e " ${BOLD}v${VERSION}${NC} — Roll out features with AI agents"
|
|
8834
9414
|
echo ""
|
|
8835
9415
|
echo "Usage: roll <command> [options]"
|
|
8836
|
-
echo "
|
|
9416
|
+
echo "$(msg backlog.usage_roll_command_options)"
|
|
8837
9417
|
echo ""
|
|
8838
9418
|
echo "Commands:"
|
|
8839
|
-
echo "
|
|
8840
|
-
echo "
|
|
8841
|
-
echo "
|
|
8842
|
-
echo "
|
|
8843
|
-
echo "
|
|
8844
|
-
echo "
|
|
8845
|
-
echo "
|
|
8846
|
-
echo "
|
|
8847
|
-
echo "
|
|
8848
|
-
echo "
|
|
8849
|
-
echo "
|
|
8850
|
-
echo "
|
|
8851
|
-
echo "
|
|
8852
|
-
echo "
|
|
8853
|
-
echo "
|
|
8854
|
-
echo "
|
|
8855
|
-
echo "
|
|
9419
|
+
echo "$(msg backlog.setup_f_machine_first_time_install)"
|
|
9420
|
+
echo "$(msg backlog.update_upgrade_npm_install_latest_re)"
|
|
9421
|
+
echo "$(msg backlog.init_project_create_agents_md_roll)"
|
|
9422
|
+
echo "$(msg backlog.offboard_confirm_project_reverse_a_previous)"
|
|
9423
|
+
echo "$(msg backlog.status_diagnostic_show_current_state)"
|
|
9424
|
+
echo "$(msg backlog.peer_peer_review_cross_agent_negotiation)"
|
|
9425
|
+
echo "$(msg backlog.loop_on_off_now_status_monitor)"
|
|
9426
|
+
echo "$(msg backlog.brief_digest_show_latest_owner_brief)"
|
|
9427
|
+
echo "$(msg backlog.backlog_view_show_pending_tasks_todo)"
|
|
9428
|
+
echo "$(msg backlog.backlog_block_pat_reason_mark_matching)"
|
|
9429
|
+
echo "$(msg backlog.backlog_defer_pat_reason_mark_matching)"
|
|
9430
|
+
echo "$(msg backlog.backlog_unblock_pat_restore_matching_items)"
|
|
9431
|
+
echo "$(msg backlog.backlog_unstick_dry_run_revert_in)"
|
|
9432
|
+
echo "$(msg backlog.backlog_lint_check_descriptions_for_path)"
|
|
9433
|
+
echo "$(msg backlog.agent_use_name_list_config_per)"
|
|
9434
|
+
echo "$(msg backlog.ci_wait_ci_show_or_wait)"
|
|
9435
|
+
echo "$(msg backlog.review_pr_number_pr_review_ai)"
|
|
8856
9436
|
echo ""
|
|
8857
|
-
echo "
|
|
8858
|
-
echo "
|
|
8859
|
-
echo "
|
|
8860
|
-
echo "
|
|
8861
|
-
echo "
|
|
8862
|
-
echo "
|
|
8863
|
-
echo "
|
|
8864
|
-
echo "
|
|
8865
|
-
echo "
|
|
8866
|
-
echo "
|
|
9437
|
+
echo "$(msg backlog.examples)"
|
|
9438
|
+
echo "$(msg backlog.roll_setup_new_machine_first_time)"
|
|
9439
|
+
echo "$(msg backlog.roll_update_upgrade_to_latest_version)"
|
|
9440
|
+
echo "$(msg backlog.roll_init_new_or_re_merge)"
|
|
9441
|
+
echo "$(msg backlog.roll_loop_on_enable_autonomous_loop)"
|
|
9442
|
+
echo "$(msg backlog.roll_brief_show_latest_brief)"
|
|
9443
|
+
echo "$(msg backlog.roll_backlog_show_pending_blocked_deferred)"
|
|
9444
|
+
echo "$(msg backlog.roll_backlog_defer_us_doc)"
|
|
9445
|
+
echo "$(msg backlog.roll_backlog_block_us_hw_001)"
|
|
9446
|
+
echo "$(msg backlog.roll_agent_use_kimi_switch_this)"
|
|
8867
9447
|
|
|
8868
9448
|
}
|
|
8869
9449
|
|